sessioner 0.1.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 +251 -0
- package/lib/actions/delete.d.ts +4 -0
- package/lib/actions/delete.js +57 -0
- package/lib/actions/fork.d.ts +2 -0
- package/lib/actions/fork.js +30 -0
- package/lib/actions/merge.d.ts +2 -0
- package/lib/actions/merge.js +106 -0
- package/lib/actions/open.d.ts +2 -0
- package/lib/actions/open.js +10 -0
- package/lib/actions/preview.d.ts +2 -0
- package/lib/actions/preview.js +56 -0
- package/lib/actions/prune.d.ts +2 -0
- package/lib/actions/prune.js +103 -0
- package/lib/actions/rename.d.ts +2 -0
- package/lib/actions/rename.js +87 -0
- package/lib/actions/trim.d.ts +2 -0
- package/lib/actions/trim.js +26 -0
- package/lib/bin/index.d.ts +2 -0
- package/lib/bin/index.js +217 -0
- package/lib/helpers/ansi.d.ts +9 -0
- package/lib/helpers/ansi.js +9 -0
- package/lib/helpers/format-date.d.ts +1 -0
- package/lib/helpers/format-date.js +8 -0
- package/lib/helpers/format-stats.d.ts +3 -0
- package/lib/helpers/format-stats.js +81 -0
- package/lib/helpers/get-columns.d.ts +1 -0
- package/lib/helpers/get-columns.js +8 -0
- package/lib/helpers/rename-cache.d.ts +3 -0
- package/lib/helpers/rename-cache.js +18 -0
- package/lib/helpers/styled-label.d.ts +2 -0
- package/lib/helpers/styled-label.js +2 -0
- package/lib/helpers/truncate.d.ts +1 -0
- package/lib/helpers/truncate.js +1 -0
- package/lib/sessions/analyze-prune.d.ts +12 -0
- package/lib/sessions/analyze-prune.js +77 -0
- package/lib/sessions/count-subagents.d.ts +2 -0
- package/lib/sessions/count-subagents.js +13 -0
- package/lib/sessions/extract-first-user-message.d.ts +1 -0
- package/lib/sessions/extract-first-user-message.js +42 -0
- package/lib/sessions/extract-messages.d.ts +5 -0
- package/lib/sessions/extract-messages.js +43 -0
- package/lib/sessions/get-entries.d.ts +1 -0
- package/lib/sessions/get-entries.js +25 -0
- package/lib/sessions/list-sessions.d.ts +2 -0
- package/lib/sessions/list-sessions.js +29 -0
- package/lib/sessions/parse-line.d.ts +2 -0
- package/lib/sessions/parse-line.js +8 -0
- package/lib/sessions/rewrite-jsonl.d.ts +1 -0
- package/lib/sessions/rewrite-jsonl.js +34 -0
- package/lib/sessions/stats.d.ts +3 -0
- package/lib/sessions/stats.js +146 -0
- package/lib/types.d.ts +100 -0
- package/lib/types.js +1 -0
- package/lib/ui/custom-select.d.ts +2 -0
- package/lib/ui/custom-select.js +113 -0
- package/lib/ui/select.d.ts +10 -0
- package/lib/ui/select.js +459 -0
- package/package.json +64 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
export const extractMessages = async (session) => {
|
|
3
|
+
const raw = await readFile(session.file, 'utf-8');
|
|
4
|
+
const rawLines = raw.split('\n');
|
|
5
|
+
const lines = [];
|
|
6
|
+
let ordinal = 0;
|
|
7
|
+
for (const line of rawLines) {
|
|
8
|
+
if (!line.trim())
|
|
9
|
+
continue;
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(line);
|
|
12
|
+
if (parsed.type !== 'user' && parsed.type !== 'assistant')
|
|
13
|
+
continue;
|
|
14
|
+
const role = parsed.type === 'user' ? '[ YOU ]' : '[ LLM ]';
|
|
15
|
+
const content = parsed.message?.content ?? [];
|
|
16
|
+
let text = '';
|
|
17
|
+
for (const block of content) {
|
|
18
|
+
if (block.type === 'text' && block.text) {
|
|
19
|
+
text = block.text;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
if (block.type === 'tool_result' && block.content) {
|
|
23
|
+
if (typeof block.content === 'string') {
|
|
24
|
+
text = block.content;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(block.content)) {
|
|
28
|
+
const sub = block.content.find((item) => item.text);
|
|
29
|
+
if (sub)
|
|
30
|
+
text = sub.text;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
text = text.replace(/\r?\n/g, ' ').trim().replace(/ {2,}/g, ' ');
|
|
36
|
+
if (text)
|
|
37
|
+
lines.push([ordinal, `${role} ${text}`]);
|
|
38
|
+
ordinal++;
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
return { total: ordinal, lines: lines.reverse() };
|
|
43
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getEntries: (dir: string) => Promise<Map<string, Date>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
export const getEntries = async (dir) => {
|
|
4
|
+
try {
|
|
5
|
+
const names = await readdir(dir);
|
|
6
|
+
const jsonl = names.filter((name) => name.endsWith('.jsonl'));
|
|
7
|
+
const files = await Promise.all(jsonl.map(async (name) => {
|
|
8
|
+
const path = join(dir, name);
|
|
9
|
+
const { mtime } = await stat(path);
|
|
10
|
+
return { file: path, mtime };
|
|
11
|
+
}));
|
|
12
|
+
files.sort((first, second) => second.mtime.getTime() - first.mtime.getTime());
|
|
13
|
+
const entries = new Map();
|
|
14
|
+
for (const { file, mtime } of files) {
|
|
15
|
+
const id = basename(file, '.jsonl');
|
|
16
|
+
if (id.startsWith('agent-'))
|
|
17
|
+
continue;
|
|
18
|
+
entries.set(file, mtime);
|
|
19
|
+
}
|
|
20
|
+
return entries;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return new Map();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { formatDate } from '../helpers/format-date.js';
|
|
4
|
+
import { extractFirstUserMessage } from './extract-first-user-message.js';
|
|
5
|
+
import { getEntries } from './get-entries.js';
|
|
6
|
+
const defaultBase = join(homedir(), '.claude', 'projects');
|
|
7
|
+
export const listSessions = async (path, base) => {
|
|
8
|
+
const projectDir = join(base ?? defaultBase, (path ?? process.cwd()).replace(/[^a-zA-Z0-9]/g, '-'));
|
|
9
|
+
const entries = await getEntries(projectDir);
|
|
10
|
+
const sessions = [];
|
|
11
|
+
const empty = [];
|
|
12
|
+
const results = await Promise.all([...entries].map(async ([file, mtime]) => {
|
|
13
|
+
const message = await extractFirstUserMessage(file);
|
|
14
|
+
return { file, mtime, message };
|
|
15
|
+
}));
|
|
16
|
+
for (const { file, mtime, message } of results) {
|
|
17
|
+
if (!message) {
|
|
18
|
+
empty.push(file);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
sessions.push({
|
|
22
|
+
id: basename(file, '.jsonl'),
|
|
23
|
+
date: formatDate(mtime),
|
|
24
|
+
message,
|
|
25
|
+
file,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return { sessions, empty };
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const rewriteJsonl: (content: string, sessionId: string) => string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export const rewriteJsonl = (content, sessionId) => {
|
|
3
|
+
const lines = content.split('\n');
|
|
4
|
+
const uuidMap = new Map();
|
|
5
|
+
for (const line of lines) {
|
|
6
|
+
if (!line.trim())
|
|
7
|
+
continue;
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(line);
|
|
10
|
+
if (parsed.uuid)
|
|
11
|
+
uuidMap.set(parsed.uuid, randomUUID());
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
}
|
|
15
|
+
return lines
|
|
16
|
+
.map((line) => {
|
|
17
|
+
if (!line.trim())
|
|
18
|
+
return line;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(line);
|
|
21
|
+
if (parsed.uuid)
|
|
22
|
+
parsed.uuid = uuidMap.get(parsed.uuid);
|
|
23
|
+
if (parsed.parentUuid)
|
|
24
|
+
parsed.parentUuid = uuidMap.get(parsed.parentUuid) ?? randomUUID();
|
|
25
|
+
if (parsed.sessionId)
|
|
26
|
+
parsed.sessionId = sessionId;
|
|
27
|
+
return JSON.stringify(parsed);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return line;
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.join('\n');
|
|
34
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { countSubagents } from './count-subagents.js';
|
|
3
|
+
const TOKENS_PER_MILLION = 1_000_000;
|
|
4
|
+
const PRICING = {
|
|
5
|
+
opus: { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
|
|
6
|
+
sonnet: { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
7
|
+
haiku: { input: 0.8, output: 4, cacheWrite: 1.0, cacheRead: 0.08 },
|
|
8
|
+
unknown: { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
9
|
+
};
|
|
10
|
+
const detectModel = (modelString) => {
|
|
11
|
+
if (modelString.includes('opus'))
|
|
12
|
+
return 'opus';
|
|
13
|
+
if (modelString.includes('haiku'))
|
|
14
|
+
return 'haiku';
|
|
15
|
+
if (modelString.includes('sonnet'))
|
|
16
|
+
return 'sonnet';
|
|
17
|
+
return 'unknown';
|
|
18
|
+
};
|
|
19
|
+
const computeCost = (tokens, model) => {
|
|
20
|
+
const pricing = PRICING[model];
|
|
21
|
+
return ((tokens.input * pricing.input +
|
|
22
|
+
tokens.output * pricing.output +
|
|
23
|
+
tokens.cacheCreation * pricing.cacheWrite +
|
|
24
|
+
tokens.cacheRead * pricing.cacheRead) /
|
|
25
|
+
TOKENS_PER_MILLION);
|
|
26
|
+
};
|
|
27
|
+
export const stats = async (session) => {
|
|
28
|
+
const raw = await readFile(session.file, 'utf-8');
|
|
29
|
+
const lines = raw.split('\n');
|
|
30
|
+
const tokens = {
|
|
31
|
+
input: 0,
|
|
32
|
+
output: 0,
|
|
33
|
+
cacheCreation: 0,
|
|
34
|
+
cacheRead: 0,
|
|
35
|
+
};
|
|
36
|
+
const perModel = new Map();
|
|
37
|
+
const tools = new Map();
|
|
38
|
+
let userCount = 0;
|
|
39
|
+
let assistantCount = 0;
|
|
40
|
+
let first = '';
|
|
41
|
+
let last = '';
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
if (!line.trim())
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(line);
|
|
47
|
+
if (parsed.timestamp) {
|
|
48
|
+
if (!first)
|
|
49
|
+
first = parsed.timestamp;
|
|
50
|
+
last = parsed.timestamp;
|
|
51
|
+
}
|
|
52
|
+
if (parsed.type === 'user')
|
|
53
|
+
userCount++;
|
|
54
|
+
if (parsed.type === 'assistant')
|
|
55
|
+
assistantCount++;
|
|
56
|
+
if (parsed.type === 'assistant' && parsed.message?.usage) {
|
|
57
|
+
const usage = parsed.message.usage;
|
|
58
|
+
const model = detectModel(parsed.message.model ?? '');
|
|
59
|
+
const input = usage.input_tokens ?? 0;
|
|
60
|
+
const output = usage.output_tokens ?? 0;
|
|
61
|
+
const cacheCreation = usage.cache_creation_input_tokens ?? 0;
|
|
62
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
63
|
+
tokens.input += input;
|
|
64
|
+
tokens.output += output;
|
|
65
|
+
tokens.cacheCreation += cacheCreation;
|
|
66
|
+
tokens.cacheRead += cacheRead;
|
|
67
|
+
const existing = perModel.get(model) ?? {
|
|
68
|
+
input: 0,
|
|
69
|
+
output: 0,
|
|
70
|
+
cacheCreation: 0,
|
|
71
|
+
cacheRead: 0,
|
|
72
|
+
};
|
|
73
|
+
existing.input += input;
|
|
74
|
+
existing.output += output;
|
|
75
|
+
existing.cacheCreation += cacheCreation;
|
|
76
|
+
existing.cacheRead += cacheRead;
|
|
77
|
+
perModel.set(model, existing);
|
|
78
|
+
}
|
|
79
|
+
if (parsed.type === 'assistant' &&
|
|
80
|
+
Array.isArray(parsed.message?.content)) {
|
|
81
|
+
for (const block of parsed.message.content) {
|
|
82
|
+
if (block.type !== 'tool_use')
|
|
83
|
+
continue;
|
|
84
|
+
const name = block.name ?? 'unknown';
|
|
85
|
+
tools.set(name, (tools.get(name) ?? 0) + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
}
|
|
91
|
+
const cost = new Map();
|
|
92
|
+
let totalCost = 0;
|
|
93
|
+
for (const [model, modelTokens] of perModel) {
|
|
94
|
+
const modelCost = computeCost(modelTokens, model);
|
|
95
|
+
cost.set(model, modelCost);
|
|
96
|
+
totalCost += modelCost;
|
|
97
|
+
}
|
|
98
|
+
const subagents = await countSubagents(session);
|
|
99
|
+
return {
|
|
100
|
+
tokens,
|
|
101
|
+
cost,
|
|
102
|
+
totalCost,
|
|
103
|
+
tools,
|
|
104
|
+
duration: { first, last },
|
|
105
|
+
messages: { user: userCount, assistant: assistantCount },
|
|
106
|
+
subagents,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
export const projectStats = async (sessions) => {
|
|
110
|
+
const totals = {
|
|
111
|
+
tokens: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
112
|
+
cost: new Map(),
|
|
113
|
+
totalCost: 0,
|
|
114
|
+
tools: new Map(),
|
|
115
|
+
duration: { first: '', last: '' },
|
|
116
|
+
messages: { user: 0, assistant: 0 },
|
|
117
|
+
subagents: 0,
|
|
118
|
+
};
|
|
119
|
+
for (const session of sessions) {
|
|
120
|
+
const result = await stats(session);
|
|
121
|
+
totals.tokens.input += result.tokens.input;
|
|
122
|
+
totals.tokens.output += result.tokens.output;
|
|
123
|
+
totals.tokens.cacheCreation += result.tokens.cacheCreation;
|
|
124
|
+
totals.tokens.cacheRead += result.tokens.cacheRead;
|
|
125
|
+
totals.messages.user += result.messages.user;
|
|
126
|
+
totals.messages.assistant += result.messages.assistant;
|
|
127
|
+
totals.subagents += result.subagents;
|
|
128
|
+
totals.totalCost += result.totalCost;
|
|
129
|
+
for (const [model, modelCost] of result.cost) {
|
|
130
|
+
totals.cost.set(model, (totals.cost.get(model) ?? 0) + modelCost);
|
|
131
|
+
}
|
|
132
|
+
for (const [tool, count] of result.tools) {
|
|
133
|
+
totals.tools.set(tool, (totals.tools.get(tool) ?? 0) + count);
|
|
134
|
+
}
|
|
135
|
+
if (result.duration.first) {
|
|
136
|
+
if (!totals.duration.first ||
|
|
137
|
+
result.duration.first < totals.duration.first)
|
|
138
|
+
totals.duration.first = result.duration.first;
|
|
139
|
+
}
|
|
140
|
+
if (result.duration.last) {
|
|
141
|
+
if (!totals.duration.last || result.duration.last > totals.duration.last)
|
|
142
|
+
totals.duration.last = result.duration.last;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return totals;
|
|
146
|
+
};
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export type MessageContent = {
|
|
2
|
+
type: string;
|
|
3
|
+
text?: string;
|
|
4
|
+
};
|
|
5
|
+
export type Message = {
|
|
6
|
+
type: string;
|
|
7
|
+
message?: {
|
|
8
|
+
content: MessageContent[];
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export type Session = {
|
|
12
|
+
id: string;
|
|
13
|
+
date: string;
|
|
14
|
+
message: string;
|
|
15
|
+
file: string;
|
|
16
|
+
};
|
|
17
|
+
export type Action = 'open' | 'fork' | 'merge' | 'prune' | 'trim' | 'rename' | 'delete' | 'stats' | 'back' | 'exit';
|
|
18
|
+
export type StyledLabelOptions = {
|
|
19
|
+
label: string;
|
|
20
|
+
icon: string;
|
|
21
|
+
iconColor?: string;
|
|
22
|
+
};
|
|
23
|
+
export type SelectOption<T> = {
|
|
24
|
+
value: T;
|
|
25
|
+
label: string;
|
|
26
|
+
hint?: string;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
separator?: boolean;
|
|
29
|
+
icon?: string;
|
|
30
|
+
iconColor?: string;
|
|
31
|
+
number?: number;
|
|
32
|
+
};
|
|
33
|
+
export type SelectConfig<T> = {
|
|
34
|
+
message: string;
|
|
35
|
+
options: SelectOption<T>[];
|
|
36
|
+
initialValue?: T;
|
|
37
|
+
numbered?: boolean;
|
|
38
|
+
shortcuts?: Map<string, number>;
|
|
39
|
+
};
|
|
40
|
+
export type ListResult = {
|
|
41
|
+
sessions: Session[];
|
|
42
|
+
empty: string[];
|
|
43
|
+
};
|
|
44
|
+
export type IndexEntry = {
|
|
45
|
+
sessionId: string;
|
|
46
|
+
firstPrompt: string;
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
};
|
|
49
|
+
export type SessionIndex = {
|
|
50
|
+
version: number;
|
|
51
|
+
entries: IndexEntry[];
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
export type RenameCache = Record<string, string>;
|
|
55
|
+
export type ContentBlock = {
|
|
56
|
+
type: string;
|
|
57
|
+
text?: string;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
};
|
|
60
|
+
export type UserLine = {
|
|
61
|
+
type: 'user';
|
|
62
|
+
message: {
|
|
63
|
+
content: ContentBlock[];
|
|
64
|
+
};
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
};
|
|
67
|
+
export type ModelName = 'opus' | 'sonnet' | 'haiku' | 'unknown';
|
|
68
|
+
export type TokenUsage = {
|
|
69
|
+
input: number;
|
|
70
|
+
output: number;
|
|
71
|
+
cacheCreation: number;
|
|
72
|
+
cacheRead: number;
|
|
73
|
+
};
|
|
74
|
+
export type SessionStats = {
|
|
75
|
+
tokens: TokenUsage;
|
|
76
|
+
cost: Map<ModelName, number>;
|
|
77
|
+
totalCost: number;
|
|
78
|
+
tools: Map<string, number>;
|
|
79
|
+
duration: {
|
|
80
|
+
first: string;
|
|
81
|
+
last: string;
|
|
82
|
+
};
|
|
83
|
+
messages: {
|
|
84
|
+
user: number;
|
|
85
|
+
assistant: number;
|
|
86
|
+
};
|
|
87
|
+
subagents: number;
|
|
88
|
+
};
|
|
89
|
+
export type PruneStats = {
|
|
90
|
+
toolBlocks: number;
|
|
91
|
+
shortMessages: number;
|
|
92
|
+
emptyMessages: number;
|
|
93
|
+
systemTags: number;
|
|
94
|
+
};
|
|
95
|
+
export type PruneOptions = {
|
|
96
|
+
toolBlocks: boolean;
|
|
97
|
+
shortMessages: boolean;
|
|
98
|
+
emptyMessages: boolean;
|
|
99
|
+
systemTags: boolean;
|
|
100
|
+
};
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { SelectPrompt } from '@clack/core';
|
|
2
|
+
import { limitOptions, S_BAR, S_BAR_END, S_RADIO_ACTIVE, S_RADIO_INACTIVE, symbol, } from '@clack/prompts';
|
|
3
|
+
import { colorize, cyan, DEFAULT_ICON_COLOR, dim, gray, green, strikethrough, } from '../helpers/ansi.js';
|
|
4
|
+
const style = (option, active) => {
|
|
5
|
+
if (option.separator)
|
|
6
|
+
return '';
|
|
7
|
+
const label = option.label;
|
|
8
|
+
if (option.disabled) {
|
|
9
|
+
const bullet = option.icon ?? S_RADIO_INACTIVE;
|
|
10
|
+
const hint = option.hint ? ` ${dim(`(${option.hint})`)}` : '';
|
|
11
|
+
return `${gray(bullet)} ${gray(label)}${hint}`;
|
|
12
|
+
}
|
|
13
|
+
const numStr = option.number !== undefined ? `[${option.number}] ` : '';
|
|
14
|
+
if (active) {
|
|
15
|
+
const bullet = option.icon ?? S_RADIO_ACTIVE;
|
|
16
|
+
const hint = option.hint ? ` ${dim(`(${option.hint})`)}` : '';
|
|
17
|
+
const styledBullet = option.icon
|
|
18
|
+
? colorize(bullet, option.iconColor ?? DEFAULT_ICON_COLOR)
|
|
19
|
+
: green(bullet);
|
|
20
|
+
const numLabel = option.number !== undefined ? `${dim(`[${option.number}]`)} ` : '';
|
|
21
|
+
return `${styledBullet} ${numLabel}${label}${hint}`;
|
|
22
|
+
}
|
|
23
|
+
const bullet = option.icon ?? S_RADIO_INACTIVE;
|
|
24
|
+
const color = option.icon ? (option.iconColor ?? DEFAULT_ICON_COLOR) : null;
|
|
25
|
+
const styledBullet = color ? colorize(bullet, color) : dim(bullet);
|
|
26
|
+
return `${styledBullet} ${dim(`${numStr}${label}`)}`;
|
|
27
|
+
};
|
|
28
|
+
export const select = (config) => {
|
|
29
|
+
const numberMap = new Map();
|
|
30
|
+
if (config.numbered) {
|
|
31
|
+
let num = 1;
|
|
32
|
+
for (let index = 0; index < config.options.length; index++) {
|
|
33
|
+
const option = config.options[index];
|
|
34
|
+
if (!option || option.separator || option.disabled)
|
|
35
|
+
continue;
|
|
36
|
+
if (option.label === 'Exit') {
|
|
37
|
+
option.number = 0;
|
|
38
|
+
numberMap.set(0, index);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
option.number = num;
|
|
42
|
+
numberMap.set(num, index);
|
|
43
|
+
num++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const prompt = new SelectPrompt({
|
|
48
|
+
options: config.options,
|
|
49
|
+
initialValue: config.initialValue,
|
|
50
|
+
render() {
|
|
51
|
+
const header = `${gray(S_BAR)}\n${symbol(this.state)} ${config.message}\n`;
|
|
52
|
+
switch (this.state) {
|
|
53
|
+
case 'submit': {
|
|
54
|
+
const selected = this.options[this.cursor];
|
|
55
|
+
if (!selected)
|
|
56
|
+
return header;
|
|
57
|
+
return `${header}${gray(S_BAR)} ${dim(selected.label)}`;
|
|
58
|
+
}
|
|
59
|
+
case 'cancel': {
|
|
60
|
+
const selected = this.options[this.cursor];
|
|
61
|
+
if (!selected)
|
|
62
|
+
return header;
|
|
63
|
+
return `${header}${gray(S_BAR)} ${strikethrough(dim(selected.label))}\n${gray(S_BAR)}`;
|
|
64
|
+
}
|
|
65
|
+
default: {
|
|
66
|
+
const prefix = `${cyan(S_BAR)} `;
|
|
67
|
+
const end = cyan(S_BAR_END);
|
|
68
|
+
const headerLines = header.split('\n').length;
|
|
69
|
+
const lines = limitOptions({
|
|
70
|
+
cursor: this.cursor,
|
|
71
|
+
options: this.options,
|
|
72
|
+
maxItems: undefined,
|
|
73
|
+
style,
|
|
74
|
+
columnPadding: prefix.length,
|
|
75
|
+
rowPadding: headerLines + 2,
|
|
76
|
+
});
|
|
77
|
+
return `${header}${prefix}${lines.join(`\n${prefix}`)}\n${end}\n`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const hasNumbered = config.numbered && numberMap.size > 0;
|
|
83
|
+
const shortcuts = config.shortcuts;
|
|
84
|
+
const hasShortcuts = shortcuts && shortcuts.size > 0;
|
|
85
|
+
if (hasNumbered || hasShortcuts) {
|
|
86
|
+
let submitting = false;
|
|
87
|
+
prompt.on('key', (char) => {
|
|
88
|
+
if (submitting || char === undefined)
|
|
89
|
+
return;
|
|
90
|
+
let targetIndex;
|
|
91
|
+
if (hasNumbered) {
|
|
92
|
+
const digit = parseInt(char);
|
|
93
|
+
if (!isNaN(digit))
|
|
94
|
+
targetIndex = numberMap.get(digit);
|
|
95
|
+
}
|
|
96
|
+
if (targetIndex === undefined && hasShortcuts) {
|
|
97
|
+
targetIndex = shortcuts.get(char);
|
|
98
|
+
}
|
|
99
|
+
if (targetIndex === undefined)
|
|
100
|
+
return;
|
|
101
|
+
const selected = config.options[targetIndex];
|
|
102
|
+
if (!selected)
|
|
103
|
+
return;
|
|
104
|
+
submitting = true;
|
|
105
|
+
prompt.cursor = targetIndex;
|
|
106
|
+
prompt.value = selected.value;
|
|
107
|
+
setImmediate(() => {
|
|
108
|
+
process.stdin.emit('keypress', '\r', { name: 'return' });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return prompt.prompt();
|
|
113
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
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>;
|
|
3
|
+
export declare const select: (sessions: Session[], project: string, base: string) => Promise<Session | "exit" | null>;
|
|
4
|
+
export declare const selectAction: (session: Session, project: string, base: string, previewLines: string[], subagentCount: number) => Promise<Action | null>;
|
|
5
|
+
export declare const selectMultiple: (sessions: Session[], project: string, base: string) => Promise<Session[] | null>;
|
|
6
|
+
export declare const promptTitle: (fields?: Record<string, string>) => Promise<string | null>;
|
|
7
|
+
export declare const promptConfirm: (message: string) => Promise<boolean | null>;
|
|
8
|
+
export declare const selectPruneOptions: (stats: PruneStats, fields?: Record<string, string>) => Promise<Set<keyof PruneStats> | "exit" | null>;
|
|
9
|
+
export declare const selectTrimLine: (messages: [number, string][], fields?: Record<string, string>) => Promise<number | null>;
|
|
10
|
+
export declare const showProjectStats: (lines: string[], fields: Record<string, string>) => Promise<void>;
|