@lzdi/pty-remote-cli 0.1.3
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/bin/pty-remote-cli.js +24 -0
- package/cli.conf +51 -0
- package/codex_template.jsonl +1 -0
- package/package.json +45 -0
- package/scripts/ensure-node-pty-helper.js +24 -0
- package/src/attachments/manager.ts +196 -0
- package/src/cli/cli-config.ts +58 -0
- package/src/cli/client.ts +674 -0
- package/src/cli/jsonl.ts +483 -0
- package/src/cli/pty-manager.ts +1509 -0
- package/src/cli/pty.ts +162 -0
- package/src/cli-main.ts +18 -0
- package/src/project-history.ts +175 -0
- package/src/providers/claude-history.ts +124 -0
- package/src/providers/claude.ts +66 -0
- package/src/providers/codex-history.ts +390 -0
- package/src/providers/codex-jsonl.ts +604 -0
- package/src/providers/codex-manager.ts +1662 -0
- package/src/providers/codex-pty.ts +144 -0
- package/src/providers/codex-resume-session.ts +253 -0
- package/src/providers/codex.ts +67 -0
- package/src/providers/provider-runtime.ts +58 -0
- package/src/providers/slash-commands.ts +115 -0
- package/src/terminal/frame-state.ts +457 -0
- package/src/threads-cli.ts +164 -0
package/src/cli/pty.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { spawn as spawnPty, type IPty } from 'node-pty';
|
|
4
|
+
|
|
5
|
+
export interface ClaudePtySession {
|
|
6
|
+
pty: IPty;
|
|
7
|
+
recentOutput: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ClaudePtyLifecycle = 'not_ready' | 'idle' | 'running';
|
|
11
|
+
|
|
12
|
+
interface StartClaudePtySessionOptions {
|
|
13
|
+
claudeBin: string;
|
|
14
|
+
cols: number;
|
|
15
|
+
cwd: string;
|
|
16
|
+
env: NodeJS.ProcessEnv;
|
|
17
|
+
permissionMode: string;
|
|
18
|
+
resumeSessionId?: string | null;
|
|
19
|
+
rows: number;
|
|
20
|
+
onData: (chunk: string) => void;
|
|
21
|
+
onExit: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createClaudeLaunchConfig(
|
|
25
|
+
claudeBin: string,
|
|
26
|
+
permissionMode: string,
|
|
27
|
+
resumeSessionId?: string | null
|
|
28
|
+
): {
|
|
29
|
+
command: string;
|
|
30
|
+
args: string[];
|
|
31
|
+
sessionId: string;
|
|
32
|
+
} {
|
|
33
|
+
if (resumeSessionId) {
|
|
34
|
+
return {
|
|
35
|
+
command: claudeBin,
|
|
36
|
+
args: ['--permission-mode', permissionMode, '--resume', resumeSessionId],
|
|
37
|
+
sessionId: resumeSessionId
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sessionId = randomUUID();
|
|
42
|
+
return {
|
|
43
|
+
command: claudeBin,
|
|
44
|
+
args: ['--permission-mode', permissionMode, '--session-id', sessionId],
|
|
45
|
+
sessionId
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function startClaudePtySession(options: StartClaudePtySessionOptions): {
|
|
50
|
+
session: ClaudePtySession;
|
|
51
|
+
sessionId: string;
|
|
52
|
+
} {
|
|
53
|
+
const launch = createClaudeLaunchConfig(options.claudeBin, options.permissionMode, options.resumeSessionId);
|
|
54
|
+
const pty = spawnPty(launch.command, launch.args, {
|
|
55
|
+
cols: options.cols,
|
|
56
|
+
rows: options.rows,
|
|
57
|
+
cwd: options.cwd,
|
|
58
|
+
env: options.env,
|
|
59
|
+
name: 'xterm-256color'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const session: ClaudePtySession = {
|
|
63
|
+
pty,
|
|
64
|
+
recentOutput: ''
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
pty.onData((chunk) => {
|
|
68
|
+
options.onData(chunk);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
pty.onExit(() => {
|
|
72
|
+
options.onExit();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
session,
|
|
77
|
+
sessionId: launch.sessionId
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function stopClaudePtySession(session: ClaudePtySession | null): void {
|
|
82
|
+
if (!session) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
session.pty.kill();
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resizeClaudePtySession(session: ClaudePtySession | null, cols: number, rows: number): void {
|
|
94
|
+
if (!session) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
session.pty.resize(cols, rows);
|
|
100
|
+
} catch {
|
|
101
|
+
// ignore resize failures during session transitions
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function appendRecentOutput(session: ClaudePtySession, chunk: string, maxChars: number): string {
|
|
106
|
+
session.recentOutput = `${session.recentOutput}${chunk}`.slice(-maxChars);
|
|
107
|
+
return session.recentOutput;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function tailOutput(text: string, maxChars = 8_000): string {
|
|
111
|
+
return normalizeOutput(text).slice(-maxChars);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeOutput(text: string): string {
|
|
115
|
+
return text
|
|
116
|
+
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
|
|
117
|
+
.replace(/\r/g, '\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const RUNNING_LINE_PATTERN = /(^|\n)\s*[*·✶✻]\s+[^\n]*?\b[\p{L}-]*ing(?:\.{3}|…)?\s*$/gimu;
|
|
121
|
+
const PROMPT_LINE_PATTERN = /(^|\n)\s*>\s*[^\n]*$/gm;
|
|
122
|
+
const PROMPT_HINT_PATTERN = /Try\s+"|--\s*INSERT\s*--|Thinking on|shift\+tab to cycle|\/ide\b/gi;
|
|
123
|
+
|
|
124
|
+
function findLastMatchIndex(pattern: RegExp, text: string): number {
|
|
125
|
+
let lastIndex = -1;
|
|
126
|
+
|
|
127
|
+
for (const match of text.matchAll(pattern)) {
|
|
128
|
+
lastIndex = match.index ?? lastIndex;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lastIndex;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getClaudePtyLifecycle(output: string): ClaudePtyLifecycle {
|
|
135
|
+
const tail = tailOutput(output);
|
|
136
|
+
const lastRunningIndex = findLastMatchIndex(RUNNING_LINE_PATTERN, tail);
|
|
137
|
+
const lastPromptIndex = findLastMatchIndex(PROMPT_LINE_PATTERN, tail);
|
|
138
|
+
const lastHintIndex = findLastMatchIndex(PROMPT_HINT_PATTERN, tail);
|
|
139
|
+
|
|
140
|
+
if (lastRunningIndex > Math.max(lastPromptIndex, lastHintIndex)) {
|
|
141
|
+
return 'running';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (lastPromptIndex >= 0 && lastHintIndex >= 0 && Math.max(lastPromptIndex, lastHintIndex) > lastRunningIndex) {
|
|
145
|
+
return 'idle';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return 'not_ready';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function looksReadyForInput(output: string): boolean {
|
|
152
|
+
return getClaudePtyLifecycle(output) === 'idle';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function isInsertModeVisible(output: string): boolean {
|
|
156
|
+
return tailOutput(output).toLowerCase().includes('-- insert --');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function looksLikeBypassPrompt(output: string): boolean {
|
|
160
|
+
const plainText = tailOutput(output).toLowerCase();
|
|
161
|
+
return plainText.includes('bypass permissions') && plainText.includes('yes, i accept');
|
|
162
|
+
}
|
package/src/cli-main.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { startCliClient } from './cli/client.ts';
|
|
2
|
+
import { runThreadsCli } from './threads-cli.ts';
|
|
3
|
+
|
|
4
|
+
async function main(): Promise<void> {
|
|
5
|
+
const [command, ...restArgs] = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
if (command === 'threads') {
|
|
8
|
+
process.exitCode = await runThreadsCli(restArgs);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
await startCliClient();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
void main().catch((error) => {
|
|
16
|
+
console.error(error);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { promises as fs, type Dirent } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import type { ChatMessage } from '@lzdi/pty-remote-protocol/runtime-types.ts';
|
|
6
|
+
import type { ProjectSessionSummary } from '@lzdi/pty-remote-protocol/protocol.ts';
|
|
7
|
+
import { parseClaudeJsonlMessages, resolveClaudeProjectFilesPath } from './cli/jsonl.ts';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MAX_SESSIONS = 12;
|
|
10
|
+
const PENDING_INPUT_LABEL = '待输入';
|
|
11
|
+
const CLAUDE_SESSION_FILE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i;
|
|
12
|
+
|
|
13
|
+
interface SessionFileEntry {
|
|
14
|
+
filePath: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
updatedAtMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizePreview(text: string): string {
|
|
20
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getUserMessageText(message: ChatMessage | undefined): string {
|
|
24
|
+
if (!message) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalizePreview(
|
|
29
|
+
message.blocks
|
|
30
|
+
.map((block) => {
|
|
31
|
+
if (block.type === 'text') {
|
|
32
|
+
return block.text;
|
|
33
|
+
}
|
|
34
|
+
return '';
|
|
35
|
+
})
|
|
36
|
+
.join(' ')
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getLatestUserTextMessage(messages: ChatMessage[]): ChatMessage | undefined {
|
|
41
|
+
return [...messages].reverse().find((message) => message.role === 'user' && Boolean(getUserMessageText(message)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getMessagePreview(message: ChatMessage | undefined): string {
|
|
45
|
+
if (!message) {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const text = message.blocks
|
|
50
|
+
.map((block) => {
|
|
51
|
+
if (block.type === 'text') {
|
|
52
|
+
return block.text;
|
|
53
|
+
}
|
|
54
|
+
if (block.type === 'tool_use') {
|
|
55
|
+
return `${block.toolName} ${block.input}`;
|
|
56
|
+
}
|
|
57
|
+
return block.content;
|
|
58
|
+
})
|
|
59
|
+
.join(' ');
|
|
60
|
+
|
|
61
|
+
return normalizePreview(text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function compactTitle(text: string): string {
|
|
65
|
+
if (text.length <= 44) {
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return `${text.slice(0, 41)}...`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function summarizeSessionFile(filePath: string, cwd: string): Promise<ProjectSessionSummary | null> {
|
|
73
|
+
const rawJsonl = await fs.readFile(filePath, 'utf8');
|
|
74
|
+
const messages = parseClaudeJsonlMessages(rawJsonl);
|
|
75
|
+
if (messages.length === 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const lastUserMessage = getLatestUserTextMessage(messages);
|
|
79
|
+
const preview = getUserMessageText(lastUserMessage) || PENDING_INPUT_LABEL;
|
|
80
|
+
const stat = await fs.stat(filePath);
|
|
81
|
+
const updatedAt = lastUserMessage?.createdAt ?? new Date(stat.mtimeMs).toISOString();
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
providerId: 'claude',
|
|
85
|
+
sessionId: path.basename(filePath, '.jsonl'),
|
|
86
|
+
cwd,
|
|
87
|
+
title: compactTitle(preview),
|
|
88
|
+
preview,
|
|
89
|
+
updatedAt,
|
|
90
|
+
messageCount: messages.length
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function listSessionFiles(projectFilesPath: string): Promise<SessionFileEntry[]> {
|
|
95
|
+
let entries: Dirent[];
|
|
96
|
+
try {
|
|
97
|
+
entries = await fs.readdir(projectFilesPath, { withFileTypes: true });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
100
|
+
if (code === 'ENOENT') {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sessionFiles: SessionFileEntry[] = [];
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!entry.isFile() || !CLAUDE_SESSION_FILE_PATTERN.test(entry.name)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const filePath = path.join(projectFilesPath, entry.name);
|
|
113
|
+
let stat;
|
|
114
|
+
try {
|
|
115
|
+
stat = await fs.stat(filePath);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
118
|
+
if (code === 'ENOENT') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
if (!stat.isFile()) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (stat.size <= 0) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
sessionFiles.push({
|
|
131
|
+
filePath,
|
|
132
|
+
sessionId: path.basename(entry.name, '.jsonl'),
|
|
133
|
+
updatedAtMs: stat.mtimeMs
|
|
134
|
+
} satisfies SessionFileEntry);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return sessionFiles.sort(
|
|
138
|
+
(left, right) => right.updatedAtMs - left.updatedAtMs || right.sessionId.localeCompare(left.sessionId)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function listProjectSessions(projectRoot: string, maxSessions = DEFAULT_MAX_SESSIONS): Promise<ProjectSessionSummary[]> {
|
|
143
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
144
|
+
const canonicalProjectRoot = await fs.realpath(resolvedProjectRoot).catch(() => resolvedProjectRoot);
|
|
145
|
+
const projectFilesPath = resolveClaudeProjectFilesPath(canonicalProjectRoot, os.homedir());
|
|
146
|
+
const normalizedMax = Number.isFinite(maxSessions) ? Math.max(1, Math.min(Math.floor(maxSessions), 50)) : DEFAULT_MAX_SESSIONS;
|
|
147
|
+
const sessionFiles = await listSessionFiles(projectFilesPath);
|
|
148
|
+
const sessions: ProjectSessionSummary[] = [];
|
|
149
|
+
|
|
150
|
+
for (const entry of sessionFiles) {
|
|
151
|
+
let summary: ProjectSessionSummary | null;
|
|
152
|
+
try {
|
|
153
|
+
summary = await summarizeSessionFile(entry.filePath, canonicalProjectRoot);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
156
|
+
if (code === 'ENOENT') {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (!summary) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
sessions.push(summary);
|
|
166
|
+
if (sessions.length >= normalizedMax) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return sessions.sort((left, right) => {
|
|
172
|
+
const timestampDiff = new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime();
|
|
173
|
+
return timestampDiff || right.sessionId.localeCompare(left.sessionId);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import type { ProjectSessionSummary } from '@lzdi/pty-remote-protocol/protocol.ts';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_SESSIONS = 12;
|
|
8
|
+
const PENDING_INPUT_LABEL = '待输入';
|
|
9
|
+
|
|
10
|
+
interface ClaudeHistoryEntry {
|
|
11
|
+
display?: unknown;
|
|
12
|
+
timestamp?: unknown;
|
|
13
|
+
project?: unknown;
|
|
14
|
+
sessionId?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ClaudeHistoryOptions {
|
|
18
|
+
historyPath?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeMaxSessions(maxSessions: number): number {
|
|
22
|
+
return Number.isFinite(maxSessions) ? Math.max(1, Math.min(Math.floor(maxSessions), 50)) : DEFAULT_MAX_SESSIONS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizePreview(text: string): string {
|
|
26
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function compactTitle(text: string): string {
|
|
30
|
+
if (text.length <= 44) {
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
return `${text.slice(0, 41)}...`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function coerceTimestampMs(value: unknown): number | null {
|
|
37
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
38
|
+
return value > 1e12 ? value : value * 1000;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === 'string') {
|
|
41
|
+
const asNumber = Number(value);
|
|
42
|
+
if (Number.isFinite(asNumber)) {
|
|
43
|
+
return asNumber > 1e12 ? asNumber : asNumber * 1000;
|
|
44
|
+
}
|
|
45
|
+
const asDate = Date.parse(value);
|
|
46
|
+
if (Number.isFinite(asDate)) {
|
|
47
|
+
return asDate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveClaudeHistoryPath(options: ClaudeHistoryOptions = {}): string {
|
|
54
|
+
return options.historyPath ?? path.join(os.homedir(), '.claude', 'history.jsonl');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function listClaudeRecentSessions(
|
|
58
|
+
maxSessions = DEFAULT_MAX_SESSIONS,
|
|
59
|
+
options: ClaudeHistoryOptions = {}
|
|
60
|
+
): Promise<ProjectSessionSummary[]> {
|
|
61
|
+
const normalizedMax = normalizeMaxSessions(maxSessions);
|
|
62
|
+
const historyPath = resolveClaudeHistoryPath(options);
|
|
63
|
+
const raw = await fs.readFile(historyPath, 'utf8').catch((error: NodeJS.ErrnoException) => {
|
|
64
|
+
if (error.code === 'ENOENT') {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
});
|
|
69
|
+
if (!raw.trim()) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const summaries = new Map<string, ProjectSessionSummary>();
|
|
74
|
+
const lines = raw.split('\n');
|
|
75
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
76
|
+
const line = lines[index]?.trim();
|
|
77
|
+
if (!line) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let parsed: ClaudeHistoryEntry;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(line) as ClaudeHistoryEntry;
|
|
84
|
+
} catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const sessionId = typeof parsed.sessionId === 'string' ? parsed.sessionId.trim() : '';
|
|
89
|
+
if (!sessionId || summaries.has(sessionId)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const cwd = typeof parsed.project === 'string' ? parsed.project.trim() : '';
|
|
94
|
+
if (!cwd) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const tsMs = coerceTimestampMs(parsed.timestamp);
|
|
99
|
+
if (tsMs === null) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const previewSource = typeof parsed.display === 'string' ? parsed.display : '';
|
|
104
|
+
const preview = normalizePreview(previewSource) || PENDING_INPUT_LABEL;
|
|
105
|
+
summaries.set(sessionId, {
|
|
106
|
+
providerId: 'claude',
|
|
107
|
+
sessionId,
|
|
108
|
+
cwd: path.resolve(cwd),
|
|
109
|
+
title: compactTitle(preview),
|
|
110
|
+
preview,
|
|
111
|
+
updatedAt: new Date(tsMs).toISOString(),
|
|
112
|
+
messageCount: 0
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (summaries.size >= normalizedMax) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [...summaries.values()].sort((left, right) => {
|
|
121
|
+
const timestampDiff = new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime();
|
|
122
|
+
return timestampDiff || right.sessionId.localeCompare(left.sessionId);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ProjectSessionSummary } from '@lzdi/pty-remote-protocol/protocol.ts';
|
|
2
|
+
|
|
3
|
+
import { listClaudeRecentSessions } from './claude-history.ts';
|
|
4
|
+
import { listProviderSlashCommands } from './slash-commands.ts';
|
|
5
|
+
import { PtyManager, type PtyManagerOptions } from '../cli/pty-manager.ts';
|
|
6
|
+
|
|
7
|
+
import type { ProviderRuntime, ProviderRuntimeCallbacks, ProviderRuntimeSelection } from './provider-runtime.ts';
|
|
8
|
+
|
|
9
|
+
export function createClaudeProviderRuntime(
|
|
10
|
+
options: PtyManagerOptions,
|
|
11
|
+
callbacks: ProviderRuntimeCallbacks
|
|
12
|
+
): ProviderRuntime {
|
|
13
|
+
const manager = new PtyManager(options, callbacks);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
providerId: 'claude',
|
|
17
|
+
activateConversation(selection: ProviderRuntimeSelection) {
|
|
18
|
+
return manager.activateConversation(selection);
|
|
19
|
+
},
|
|
20
|
+
cleanupConversation(target) {
|
|
21
|
+
return manager.cleanupConversation(target);
|
|
22
|
+
},
|
|
23
|
+
cleanupProject(cwd: string) {
|
|
24
|
+
return manager.cleanupProject(cwd);
|
|
25
|
+
},
|
|
26
|
+
dispatchMessage(content: string) {
|
|
27
|
+
return manager.dispatchMessage(content);
|
|
28
|
+
},
|
|
29
|
+
getOlderMessages(beforeMessageId?: string, maxMessages?: number) {
|
|
30
|
+
return manager.getOlderMessages(beforeMessageId, maxMessages);
|
|
31
|
+
},
|
|
32
|
+
getRegistrationPayload() {
|
|
33
|
+
return manager.getRegistrationPayload();
|
|
34
|
+
},
|
|
35
|
+
getSnapshot() {
|
|
36
|
+
return manager.getSnapshot();
|
|
37
|
+
},
|
|
38
|
+
listSlashCommands() {
|
|
39
|
+
return listProviderSlashCommands('claude');
|
|
40
|
+
},
|
|
41
|
+
listProjectConversations(_projectRoot: string, maxSessions?: number): Promise<ProjectSessionSummary[]> {
|
|
42
|
+
return listClaudeRecentSessions(maxSessions);
|
|
43
|
+
},
|
|
44
|
+
listManagedPtyHandles() {
|
|
45
|
+
return Promise.resolve(manager.listManagedPtyHandles());
|
|
46
|
+
},
|
|
47
|
+
primeActiveTerminalFrame() {
|
|
48
|
+
return manager.primeActiveTerminalFrame();
|
|
49
|
+
},
|
|
50
|
+
refreshActiveState() {
|
|
51
|
+
return manager.refreshActiveState();
|
|
52
|
+
},
|
|
53
|
+
resetActiveConversation() {
|
|
54
|
+
return manager.resetActiveThread();
|
|
55
|
+
},
|
|
56
|
+
shutdown() {
|
|
57
|
+
return manager.shutdown();
|
|
58
|
+
},
|
|
59
|
+
stopActiveRun() {
|
|
60
|
+
return manager.stopActiveRun();
|
|
61
|
+
},
|
|
62
|
+
updateTerminalSize(cols: number, rows: number) {
|
|
63
|
+
manager.updateTerminalSize(cols, rows);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|