sessioner 0.1.1 โ†’ 0.1.2

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 CHANGED
@@ -56,7 +56,7 @@ sessioner
56
56
  ## ๐Ÿ” How It Works
57
57
 
58
58
  1. Scans `{base}/{project}` for `.jsonl` session files
59
- 2. Shows a **main menu** (Sessions, Stats, Clean)
59
+ 2. Shows a **main menu** (Sessions, Search, Stats, Clean)
60
60
  3. Shows a **paginated session list** sorted by date (newest first)
61
61
  4. Displays a **conversation preview** for the selected session (up to 10 messages)
62
62
  5. Opens an **action menu** to operate on it
@@ -87,6 +87,19 @@ sessioner
87
87
 
88
88
  ---
89
89
 
90
+ ### Search (across all sessions)
91
+
92
+ - Text input for a search query
93
+ - Streams all sessions in parallel for case-insensitive text matching
94
+ - Results grouped by session title, with match count and date
95
+ - Matched text highlighted in bold
96
+ - Paginated 10 matches at a time
97
+ - Clicking a match opens the session's action menu
98
+ - Back from the action menu returns to search results
99
+ - Shows "No results" when nothing matches
100
+
101
+ ---
102
+
90
103
  ### Fork
91
104
 
92
105
  - Creates an independent copy of the entire session
package/lib/bin/index.js CHANGED
@@ -15,7 +15,9 @@ import { analyze } from '../sessions/analyze-prune.js';
15
15
  import { countSubagents } from '../sessions/count-subagents.js';
16
16
  import { extractMessages } from '../sessions/extract-messages.js';
17
17
  import { listSessions } from '../sessions/list-sessions.js';
18
+ import { searchSessions } from '../sessions/search-sessions.js';
18
19
  import { projectStats, stats } from '../sessions/stats.js';
20
+ import { promptSearch, selectSearchResult, showNoResults, } from '../ui/search.js';
19
21
  import { promptConfirm, promptTitle, select, selectAction, selectMain, selectMultiple, selectPruneOptions, selectTrimLine, showProjectStats, } from '../ui/select.js';
20
22
  const usage = [
21
23
  'Usage: sessioner [options]',
@@ -50,6 +52,147 @@ if (process.stdin.isTTY) {
50
52
  }
51
53
  const project = values.project ?? process.cwd();
52
54
  const base = values.base ?? join(homedir(), '.claude', 'projects');
55
+ const handleSession = async (initial, allSessions, project, base) => {
56
+ let session = initial;
57
+ let previewLines = await preview(session);
58
+ let subagentCount = await countSubagents(session);
59
+ action: while (true) {
60
+ const action = await selectAction(session, project, base, previewLines, subagentCount);
61
+ if (!action)
62
+ return 'back';
63
+ switch (action) {
64
+ case 'back':
65
+ return 'back';
66
+ case 'rename': {
67
+ const title = await promptTitle({
68
+ Project: project,
69
+ Base: base,
70
+ ID: session.id,
71
+ Date: session.date,
72
+ Title: session.message,
73
+ });
74
+ if (!title)
75
+ continue action;
76
+ await rename(session, title);
77
+ session.message = title;
78
+ continue action;
79
+ }
80
+ case 'open':
81
+ open(session);
82
+ continue action;
83
+ case 'stats': {
84
+ const sessionStats = await stats(session);
85
+ previewLines = formatStats(sessionStats);
86
+ continue action;
87
+ }
88
+ case 'fork': {
89
+ const defaultTitle = `Fork: ${session.message}`;
90
+ const forked = await fork(session, defaultTitle);
91
+ await rename(forked, defaultTitle);
92
+ const title = await promptTitle({
93
+ Project: project,
94
+ Base: base,
95
+ ID: forked.id,
96
+ Date: forked.date,
97
+ Title: forked.message,
98
+ });
99
+ if (title) {
100
+ await rename(forked, title);
101
+ forked.message = title;
102
+ }
103
+ session = forked;
104
+ previewLines = await preview(session);
105
+ subagentCount = await countSubagents(session);
106
+ continue action;
107
+ }
108
+ case 'merge': {
109
+ const current = session;
110
+ const others = await selectMultiple(allSessions.filter((session) => session.id !== current.id), project, base);
111
+ if (!others || others.length === 0)
112
+ continue action;
113
+ const all = [current, ...others];
114
+ const titles = all.map((session) => session.message);
115
+ const defaultTitle = `Merge: ${titles.join(', ')}`;
116
+ const merged = await merge(all, defaultTitle);
117
+ await rename(merged, defaultTitle);
118
+ const title = await promptTitle({
119
+ Project: project,
120
+ Base: base,
121
+ ID: merged.id,
122
+ Date: merged.date,
123
+ Title: merged.message,
124
+ });
125
+ if (title) {
126
+ await rename(merged, title);
127
+ merged.message = title;
128
+ }
129
+ session = merged;
130
+ previewLines = await preview(session);
131
+ subagentCount = await countSubagents(session);
132
+ continue action;
133
+ }
134
+ case 'prune': {
135
+ const pruneStats = await analyze(session);
136
+ const selected = await selectPruneOptions(pruneStats, {
137
+ Project: project,
138
+ Base: base,
139
+ ID: session.id,
140
+ Date: session.date,
141
+ Title: session.message,
142
+ });
143
+ if (selected === 'exit')
144
+ return 'exit';
145
+ if (!selected)
146
+ continue action;
147
+ await prune(session, {
148
+ toolBlocks: selected.has('toolBlocks'),
149
+ shortMessages: selected.has('shortMessages'),
150
+ emptyMessages: selected.has('emptyMessages'),
151
+ systemTags: selected.has('systemTags'),
152
+ customTitles: selected.has('customTitles'),
153
+ });
154
+ previewLines = await preview(session);
155
+ subagentCount = await countSubagents(session);
156
+ continue action;
157
+ }
158
+ case 'trim': {
159
+ const { total, lines } = await extractMessages(session);
160
+ if (lines.length <= 1)
161
+ continue action;
162
+ const selected = await selectTrimLine(lines, {
163
+ Project: project,
164
+ Base: base,
165
+ ID: session.id,
166
+ Date: session.date,
167
+ Title: session.message,
168
+ });
169
+ if (selected === null)
170
+ continue action;
171
+ const keepCount = selected + 1;
172
+ const removeCount = total - keepCount;
173
+ if (removeCount <= 0)
174
+ continue action;
175
+ const confirmed = await promptConfirm(`Remove last ${removeCount} of ${total} messages?`);
176
+ if (!confirmed)
177
+ continue action;
178
+ await trim(session, keepCount);
179
+ previewLines = await preview(session);
180
+ subagentCount = await countSubagents(session);
181
+ continue action;
182
+ }
183
+ case 'delete': {
184
+ const items = await targets(session);
185
+ const confirmed = await promptConfirm(`Delete ${items.join(' + ')}?`);
186
+ if (!confirmed)
187
+ continue action;
188
+ await remove(session);
189
+ return 'back';
190
+ }
191
+ case 'exit':
192
+ return 'exit';
193
+ }
194
+ }
195
+ };
53
196
  menu: while (true) {
54
197
  const { sessions, empty } = await listSessions(project, base);
55
198
  const choice = await selectMain(project, base, empty.length);
@@ -68,6 +211,30 @@ menu: while (true) {
68
211
  await cleanEmpty(empty);
69
212
  continue menu;
70
213
  }
214
+ if (choice === 'search') {
215
+ const fields = { Project: project, Base: base };
216
+ const query = await promptSearch(fields);
217
+ if (!query)
218
+ continue menu;
219
+ search: while (true) {
220
+ const { sessions: current } = await listSessions(project, base);
221
+ const results = await searchSessions(current, query);
222
+ if (results.length === 0) {
223
+ await showNoResults(query, fields);
224
+ continue menu;
225
+ }
226
+ const selected = await selectSearchResult(results, query, fields);
227
+ if (!selected)
228
+ continue menu;
229
+ if (selected === 'exit')
230
+ break menu;
231
+ const { sessions: fresh } = await listSessions(project, base);
232
+ const actionResult = await handleSession(selected, fresh, project, base);
233
+ if (actionResult === 'exit')
234
+ break menu;
235
+ continue search;
236
+ }
237
+ }
71
238
  list: while (true) {
72
239
  const { sessions: fresh } = await listSessions(project, base);
73
240
  const result = await select(fresh, project, base);
@@ -75,144 +242,9 @@ menu: while (true) {
75
242
  continue menu;
76
243
  if (result === 'exit')
77
244
  break menu;
78
- let session = result;
79
- let previewLines = await preview(session);
80
- let subagentCount = await countSubagents(session);
81
- action: while (true) {
82
- const action = await selectAction(session, project, base, previewLines, subagentCount);
83
- if (!action)
84
- continue list;
85
- switch (action) {
86
- case 'back':
87
- continue list;
88
- case 'rename': {
89
- const title = await promptTitle({
90
- Project: project,
91
- Base: base,
92
- ID: session.id,
93
- Date: session.date,
94
- Title: session.message,
95
- });
96
- if (!title)
97
- continue action;
98
- await rename(session, title);
99
- session.message = title;
100
- continue action;
101
- }
102
- case 'open':
103
- open(session);
104
- continue action;
105
- case 'stats': {
106
- const sessionStats = await stats(session);
107
- previewLines = formatStats(sessionStats);
108
- continue action;
109
- }
110
- case 'fork': {
111
- const defaultTitle = `Fork: ${session.message}`;
112
- const forked = await fork(session, defaultTitle);
113
- await rename(forked, defaultTitle);
114
- const title = await promptTitle({
115
- Project: project,
116
- Base: base,
117
- ID: forked.id,
118
- Date: forked.date,
119
- Title: forked.message,
120
- });
121
- if (title) {
122
- await rename(forked, title);
123
- forked.message = title;
124
- }
125
- session = forked;
126
- previewLines = await preview(session);
127
- subagentCount = await countSubagents(session);
128
- continue action;
129
- }
130
- case 'merge': {
131
- const current = session;
132
- const others = await selectMultiple(fresh.filter((session) => session.id !== current.id), project, base);
133
- if (!others || others.length === 0)
134
- continue action;
135
- const all = [current, ...others];
136
- const titles = all.map((session) => session.message);
137
- const defaultTitle = `Merge: ${titles.join(', ')}`;
138
- const merged = await merge(all, defaultTitle);
139
- await rename(merged, defaultTitle);
140
- const title = await promptTitle({
141
- Project: project,
142
- Base: base,
143
- ID: merged.id,
144
- Date: merged.date,
145
- Title: merged.message,
146
- });
147
- if (title) {
148
- await rename(merged, title);
149
- merged.message = title;
150
- }
151
- session = merged;
152
- previewLines = await preview(session);
153
- subagentCount = await countSubagents(session);
154
- continue action;
155
- }
156
- case 'prune': {
157
- const stats = await analyze(session);
158
- const selected = await selectPruneOptions(stats, {
159
- Project: project,
160
- Base: base,
161
- ID: session.id,
162
- Date: session.date,
163
- Title: session.message,
164
- });
165
- if (selected === 'exit')
166
- break menu;
167
- if (!selected)
168
- continue action;
169
- await prune(session, {
170
- toolBlocks: selected.has('toolBlocks'),
171
- shortMessages: selected.has('shortMessages'),
172
- emptyMessages: selected.has('emptyMessages'),
173
- systemTags: selected.has('systemTags'),
174
- customTitles: selected.has('customTitles'),
175
- });
176
- previewLines = await preview(session);
177
- subagentCount = await countSubagents(session);
178
- continue action;
179
- }
180
- case 'trim': {
181
- const { total, lines } = await extractMessages(session);
182
- if (lines.length <= 1)
183
- continue action;
184
- const selected = await selectTrimLine(lines, {
185
- Project: project,
186
- Base: base,
187
- ID: session.id,
188
- Date: session.date,
189
- Title: session.message,
190
- });
191
- if (selected === null)
192
- continue action;
193
- const keepCount = selected + 1;
194
- const removeCount = total - keepCount;
195
- if (removeCount <= 0)
196
- continue action;
197
- const confirmed = await promptConfirm(`Remove last ${removeCount} of ${total} messages?`);
198
- if (!confirmed)
199
- continue action;
200
- await trim(session, keepCount);
201
- previewLines = await preview(session);
202
- subagentCount = await countSubagents(session);
203
- continue action;
204
- }
205
- case 'delete': {
206
- const items = await targets(session);
207
- const confirmed = await promptConfirm(`Delete ${items.join(' + ')}?`);
208
- if (!confirmed)
209
- continue action;
210
- await remove(session);
211
- continue list;
212
- }
213
- case 'exit':
214
- break menu;
215
- }
216
- }
245
+ const actionResult = await handleSession(result, fresh, project, base);
246
+ if (actionResult === 'exit')
247
+ break menu;
248
+ continue list;
217
249
  }
218
250
  }
@@ -0,0 +1,2 @@
1
+ import type { SearchMatch, Session } from '../types.js';
2
+ export declare const searchSessions: (sessions: Session[], query: string) => Promise<SearchMatch[]>;
@@ -0,0 +1,51 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ import { parseLine } from './parse-line.js';
4
+ const MIN_TEXT_LENGTH = 5;
5
+ const searchSession = (session, query) => new Promise((resolve) => {
6
+ const stream = createReadStream(session.file, 'utf-8');
7
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
8
+ const matches = [];
9
+ const lower = query.toLowerCase();
10
+ const close = () => {
11
+ resolve(matches);
12
+ reader.close();
13
+ stream.destroy();
14
+ };
15
+ reader.on('line', (line) => {
16
+ if (!line.trim())
17
+ return;
18
+ const entry = parseLine(line);
19
+ if (!entry)
20
+ return;
21
+ if (entry.type !== 'user' && entry.type !== 'assistant')
22
+ return;
23
+ const texts = entry.message?.content ?? [];
24
+ for (const block of texts) {
25
+ if (block.type !== 'text' || !block.text)
26
+ continue;
27
+ const text = block.text
28
+ .replace(/\r?\n/g, ' ')
29
+ .trim()
30
+ .replace(/ {2,}/g, ' ');
31
+ if (text.startsWith('<'))
32
+ continue;
33
+ if (text.length <= MIN_TEXT_LENGTH)
34
+ continue;
35
+ if (!text.toLowerCase().includes(lower))
36
+ continue;
37
+ const tag = entry.type === 'user' ? '[ YOU ]' : '[ LLM ]';
38
+ matches.push(`${tag} ${text}`);
39
+ break;
40
+ }
41
+ });
42
+ reader.on('close', () => resolve(matches));
43
+ stream.on('error', () => close());
44
+ });
45
+ export const searchSessions = async (sessions, query) => {
46
+ const results = await Promise.all(sessions.map(async (session) => ({
47
+ session,
48
+ entries: await searchSession(session, query),
49
+ })));
50
+ return results.filter((result) => result.entries.length > 0);
51
+ };
package/lib/types.d.ts CHANGED
@@ -92,3 +92,8 @@ export type PruneOptions = {
92
92
  systemTags: boolean;
93
93
  customTitles: boolean;
94
94
  };
95
+ export type SearchMatch = {
96
+ session: Session;
97
+ entries: string[];
98
+ };
99
+ export type ActionResult = 'back' | 'exit';
@@ -0,0 +1,16 @@
1
+ import type { SelectOption } from '../types.js';
2
+ export declare const PAGE_SIZE = 10;
3
+ export declare const NEXT = -1;
4
+ export declare const PREVIOUS = -2;
5
+ export declare const EXIT = -3;
6
+ export declare const CONFIRM = -4;
7
+ export declare const CANCEL = -5;
8
+ export declare const HEADER_MARGIN = 13;
9
+ export declare const CLACK_PREFIX = 6;
10
+ export declare const CHECKBOX_WIDTH = 2;
11
+ export declare const EXIT_NUMBER = 0;
12
+ export declare const CANCEL_NUMBER = 0;
13
+ export declare const buildShortcuts: <T>(options: SelectOption<T>[]) => Map<string, number>;
14
+ export declare const clear: () => void;
15
+ export declare const printHeader: (fields: Record<string, string>) => void;
16
+ export declare const showHeader: (fields?: Record<string, string>) => void;
@@ -0,0 +1,46 @@
1
+ import { dim } from '../helpers/ansi.js';
2
+ import { getColumns } from '../helpers/get-columns.js';
3
+ import { truncate } from '../helpers/truncate.js';
4
+ export const PAGE_SIZE = 10;
5
+ export const NEXT = -1;
6
+ export const PREVIOUS = -2;
7
+ export const EXIT = -3;
8
+ export const CONFIRM = -4;
9
+ export const CANCEL = -5;
10
+ export const HEADER_MARGIN = 13; // terminal right-side safety margin
11
+ export const CLACK_PREFIX = 6; // @clack/prompts left gutter width
12
+ export const CHECKBOX_WIDTH = 2; // "โ˜‘ " / "โ˜ " prefix
13
+ export const EXIT_NUMBER = 0;
14
+ export const CANCEL_NUMBER = 0;
15
+ export const buildShortcuts = (options) => {
16
+ const shortcuts = new Map();
17
+ for (let index = 0; index < options.length; index++) {
18
+ const option = options[index];
19
+ if (!option || option.number === undefined)
20
+ continue;
21
+ shortcuts.set(String(option.number), index);
22
+ }
23
+ return shortcuts;
24
+ };
25
+ export const clear = () => {
26
+ process.stdout.write('\x1b[2J\x1b[H');
27
+ };
28
+ export const printHeader = (fields) => {
29
+ clear();
30
+ const columns = getColumns();
31
+ const longest = Math.max(...Object.keys(fields).map((key) => key.length));
32
+ console.log();
33
+ for (const [key, value] of Object.entries(fields)) {
34
+ const padding = ' '.repeat(longest - key.length + 2);
35
+ const prefixLength = 2 + key.length + 1 + padding.length;
36
+ const available = columns - prefixLength - HEADER_MARGIN;
37
+ const truncated = available > 0 ? truncate(value, available) : value;
38
+ console.log(` ${dim(`${key}:`)}${padding}${truncated}`);
39
+ }
40
+ console.log();
41
+ };
42
+ export const showHeader = (fields) => {
43
+ if (!fields)
44
+ return clear();
45
+ printHeader(fields);
46
+ };
@@ -0,0 +1,4 @@
1
+ import type { SearchMatch, Session } from '../types.js';
2
+ export declare const promptSearch: (fields?: Record<string, string>) => Promise<string | null>;
3
+ export declare const showNoResults: (query: string, fields?: Record<string, string>) => Promise<void>;
4
+ export declare const selectSearchResult: (results: SearchMatch[], query: string, fields?: Record<string, string>) => Promise<Session | "exit" | null>;
@@ -0,0 +1,157 @@
1
+ import { isCancel, text } from '@clack/prompts';
2
+ import { bold, colorize } from '../helpers/ansi.js';
3
+ import { getColumns } from '../helpers/get-columns.js';
4
+ import { truncate } from '../helpers/truncate.js';
5
+ import { select as customSelect } from './custom-select.js';
6
+ import { buildShortcuts, CANCEL, CLACK_PREFIX, clear, EXIT, EXIT_NUMBER, HEADER_MARGIN, NEXT, PAGE_SIZE, PREVIOUS, showHeader, } from './layout.js';
7
+ const MATCH_COLOR = '38;5;208';
8
+ const SESSION_ICON = 'โ—Œ';
9
+ const TITLE_MAX_LENGTH = 100;
10
+ const INDENT = ' ';
11
+ const INDENT_WIDTH = 2;
12
+ export const promptSearch = async (fields) => {
13
+ showHeader(fields);
14
+ const result = await text({
15
+ message: 'Search query (Esc to cancel)',
16
+ placeholder: 'Type to search across all sessions...',
17
+ });
18
+ if (isCancel(result))
19
+ return null;
20
+ const trimmed = result.trim();
21
+ if (!trimmed)
22
+ return null;
23
+ return trimmed;
24
+ };
25
+ export const showNoResults = async (query, fields) => {
26
+ showHeader(fields);
27
+ await customSelect({
28
+ message: `No results for "${query}"`,
29
+ options: [
30
+ { label: '', value: 'back', disabled: true, separator: true },
31
+ { label: 'Back', value: 'back', icon: 'โ†' },
32
+ ],
33
+ numbered: true,
34
+ });
35
+ };
36
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
+ const highlightMatch = (value, query) => {
38
+ const pattern = new RegExp(escapeRegExp(query), 'gi');
39
+ return value.replace(pattern, (match) => colorize(match, MATCH_COLOR));
40
+ };
41
+ const flattenMatches = (results) => {
42
+ const entries = [];
43
+ for (const result of results) {
44
+ for (const line of result.entries) {
45
+ entries.push({ session: result.session, text: line });
46
+ }
47
+ }
48
+ return entries;
49
+ };
50
+ export const selectSearchResult = async (results, query, fields) => {
51
+ const entries = flattenMatches(results);
52
+ let page = 0;
53
+ const total = Math.ceil(entries.length / PAGE_SIZE);
54
+ const matchCounts = new Map();
55
+ for (const result of results) {
56
+ matchCounts.set(result.session.id, result.entries.length);
57
+ }
58
+ pages: while (true) {
59
+ showHeader(fields);
60
+ const start = page * PAGE_SIZE;
61
+ const slice = entries.slice(start, start + PAGE_SIZE);
62
+ const columns = getColumns();
63
+ const labelMax = columns - CLACK_PREFIX - HEADER_MARGIN;
64
+ const options = [];
65
+ let lastSessionId = '';
66
+ for (let index = 0; index < slice.length; index++) {
67
+ const entry = slice[index];
68
+ if (!entry)
69
+ continue;
70
+ if (entry.session.id !== lastSessionId) {
71
+ if (options.length > 0) {
72
+ options.push({
73
+ label: '',
74
+ value: EXIT,
75
+ disabled: true,
76
+ separator: true,
77
+ });
78
+ }
79
+ lastSessionId = entry.session.id;
80
+ const count = matchCounts.get(entry.session.id) ?? 0;
81
+ const suffix = count === 1 ? 'match' : 'matches';
82
+ options.push({
83
+ label: bold(truncate(entry.session.message, TITLE_MAX_LENGTH)),
84
+ value: EXIT,
85
+ disabled: true,
86
+ icon: colorize(SESSION_ICON, MATCH_COLOR),
87
+ hint: `${count} ${suffix} ยท ${entry.session.date}`,
88
+ });
89
+ }
90
+ const truncated = truncate(entry.text, labelMax - INDENT_WIDTH);
91
+ const highlighted = highlightMatch(truncated, query);
92
+ options.push({
93
+ label: `${INDENT}${highlighted}`,
94
+ value: start + index,
95
+ });
96
+ }
97
+ options.push({ label: '', value: EXIT, disabled: true, separator: true });
98
+ let actionNumber = 1;
99
+ if (page > 0) {
100
+ options.push({
101
+ label: 'Previous',
102
+ value: PREVIOUS,
103
+ icon: 'โ†',
104
+ number: actionNumber,
105
+ });
106
+ actionNumber++;
107
+ }
108
+ if (page < total - 1) {
109
+ options.push({
110
+ label: 'Next',
111
+ value: NEXT,
112
+ icon: 'โ†’',
113
+ number: actionNumber,
114
+ });
115
+ actionNumber++;
116
+ }
117
+ options.push({ label: '', value: EXIT, disabled: true, separator: true });
118
+ options.push({
119
+ label: 'Back to menu',
120
+ value: EXIT,
121
+ icon: 'โ˜ฐ',
122
+ number: actionNumber,
123
+ });
124
+ options.push({
125
+ label: 'Exit',
126
+ value: CANCEL,
127
+ icon: 'โœ•',
128
+ iconColor: '38;5;204',
129
+ number: EXIT_NUMBER,
130
+ });
131
+ const result = await customSelect({
132
+ message: `Search results (${page + 1}/${total})`,
133
+ options,
134
+ shortcuts: buildShortcuts(options),
135
+ });
136
+ if (isCancel(result) || result === undefined)
137
+ return null;
138
+ if (result === EXIT)
139
+ return null;
140
+ if (result === CANCEL)
141
+ return 'exit';
142
+ if (result === NEXT) {
143
+ page++;
144
+ clear();
145
+ continue pages;
146
+ }
147
+ if (result === PREVIOUS) {
148
+ page--;
149
+ clear();
150
+ continue pages;
151
+ }
152
+ const selected = entries[result];
153
+ if (!selected)
154
+ continue pages;
155
+ return selected.session;
156
+ }
157
+ };
@@ -1,5 +1,5 @@
1
1
  import type { Action, PruneStats, Session } from '../types.js';
2
- export declare const selectMain: (project: string, base: string, emptyCount?: number) => Promise<"sessions" | "stats" | "clean" | null>;
2
+ export declare const selectMain: (project: string, base: string, emptyCount?: number) => Promise<"sessions" | "search" | "stats" | "clean" | null>;
3
3
  export declare const select: (sessions: Session[], project: string, base: string) => Promise<Session | "exit" | null>;
4
4
  export declare const selectAction: (session: Session, project: string, base: string, previewLines: string[], subagentCount: number) => Promise<Action | null>;
5
5
  export declare const selectMultiple: (sessions: Session[], project: string, base: string) => Promise<Session[] | null>;
package/lib/ui/select.js CHANGED
@@ -1,55 +1,13 @@
1
1
  import { isCancel, text } from '@clack/prompts';
2
- import { dim } from '../helpers/ansi.js';
3
2
  import { getColumns } from '../helpers/get-columns.js';
4
3
  import { truncate } from '../helpers/truncate.js';
5
4
  import { select as customSelect } from './custom-select.js';
6
- const PAGE_SIZE = 10;
7
- const NEXT = -1;
8
- const PREVIOUS = -2;
9
- const EXIT = -3;
10
- const CONFIRM = -4;
11
- const CANCEL = -5;
12
- const HEADER_MARGIN = 13; // terminal right-side safety margin
13
- const CLACK_PREFIX = 6; // @clack/prompts left gutter width
14
- const CHECKBOX_WIDTH = 2; // "โ˜‘ " / "โ˜ " prefix
15
- const EXIT_NUMBER = 0;
16
- const CANCEL_NUMBER = 0;
17
- const buildShortcuts = (options) => {
18
- const shortcuts = new Map();
19
- for (let index = 0; index < options.length; index++) {
20
- const option = options[index];
21
- if (!option || option.number === undefined)
22
- continue;
23
- shortcuts.set(String(option.number), index);
24
- }
25
- return shortcuts;
26
- };
27
- const clear = () => {
28
- process.stdout.write('\x1b[2J\x1b[H');
29
- };
30
- const printHeader = (fields) => {
31
- clear();
32
- const columns = getColumns();
33
- const longest = Math.max(...Object.keys(fields).map((key) => key.length));
34
- console.log();
35
- for (const [key, value] of Object.entries(fields)) {
36
- const padding = ' '.repeat(longest - key.length + 2);
37
- const prefixLength = 2 + key.length + 1 + padding.length;
38
- const available = columns - prefixLength - HEADER_MARGIN;
39
- const truncated = available > 0 ? truncate(value, available) : value;
40
- console.log(` ${dim(`${key}:`)}${padding}${truncated}`);
41
- }
42
- console.log();
43
- };
44
- const showHeader = (fields) => {
45
- if (!fields)
46
- return clear();
47
- printHeader(fields);
48
- };
5
+ import { buildShortcuts, CANCEL, CANCEL_NUMBER, CHECKBOX_WIDTH, CLACK_PREFIX, clear, CONFIRM, EXIT, EXIT_NUMBER, HEADER_MARGIN, NEXT, PAGE_SIZE, PREVIOUS, printHeader, showHeader, } from './layout.js';
49
6
  export const selectMain = async (project, base, emptyCount = 0) => {
50
7
  printHeader({ Project: project, Base: base });
51
8
  const options = [
52
9
  { label: 'Sessions', value: 'sessions' },
10
+ { label: 'Search', value: 'search' },
53
11
  { label: 'Stats', value: 'stats' },
54
12
  ];
55
13
  if (emptyCount > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessioner",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "โœจ An interactive CLI to navigate and manage Claude Code sessions from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",