@jxtools/promptline 1.3.13 → 1.3.15
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/promptline.mjs +49 -1
- package/package.json +1 -1
- package/promptline-prompt-queue.sh +1 -1
- package/promptline-session-end.sh +9 -7
- package/src/backend/queue-store.ts +3 -7
- package/src/components/PromptCard.tsx +7 -2
- package/src/components/SessionSection.tsx +5 -5
- package/src/index.css +1 -0
- package/src/types/queue.ts +1 -1
- package/vite-plugin-api.ts +1 -1
package/bin/promptline.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, copyFileSync, chmodSync } from 'fs'
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, chmodSync, readdirSync, mkdirSync, renameSync, unlinkSync } from 'fs'
|
|
4
4
|
import { resolve, dirname, join } from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
6
|
import { spawn, execSync } from 'child_process'
|
|
@@ -94,11 +94,59 @@ vite.stderr.on('data', (data) => {
|
|
|
94
94
|
vite.on('close', (code) => process.exit(code ?? 0))
|
|
95
95
|
|
|
96
96
|
process.on('SIGINT', () => {
|
|
97
|
+
cancelAllPendingPrompts()
|
|
97
98
|
vite.kill('SIGINT')
|
|
98
99
|
console.log('\n\x1b[33m⏹\x1b[0m PromptLine stopped.')
|
|
99
100
|
process.exit(0)
|
|
100
101
|
})
|
|
101
102
|
|
|
103
|
+
// --- Cleanup ---
|
|
104
|
+
|
|
105
|
+
function cancelAllPendingPrompts() {
|
|
106
|
+
const queuesDir = join(homedir(), '.promptline', 'queues')
|
|
107
|
+
let projectDirs
|
|
108
|
+
try {
|
|
109
|
+
projectDirs = readdirSync(queuesDir, { withFileTypes: true }).filter(d => d.isDirectory())
|
|
110
|
+
} catch {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const now = new Date().toISOString()
|
|
115
|
+
for (const dir of projectDirs) {
|
|
116
|
+
const projectPath = join(queuesDir, dir.name)
|
|
117
|
+
let files
|
|
118
|
+
try {
|
|
119
|
+
files = readdirSync(projectPath).filter(f => f.endsWith('.json'))
|
|
120
|
+
} catch {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
const filePath = join(projectPath, file)
|
|
126
|
+
try {
|
|
127
|
+
const data = JSON.parse(readFileSync(filePath, 'utf-8'))
|
|
128
|
+
if (data.closedAt) continue
|
|
129
|
+
let changed = false
|
|
130
|
+
for (const p of data.prompts || []) {
|
|
131
|
+
if (p.status === 'pending' || p.status === 'running') {
|
|
132
|
+
p.status = 'cancelled'
|
|
133
|
+
p.completedAt = now
|
|
134
|
+
changed = true
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (changed) {
|
|
138
|
+
data.lastActivity = now
|
|
139
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`
|
|
140
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2))
|
|
141
|
+
renameSync(tmpPath, filePath)
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
// --- Helpers ---
|
|
103
151
|
|
|
104
152
|
function installHooks() {
|
package/package.json
CHANGED
|
@@ -176,7 +176,7 @@ try:
|
|
|
176
176
|
p["completedAt"] = now
|
|
177
177
|
|
|
178
178
|
# Step 1b: Track completedAt when all prompts are done
|
|
179
|
-
all_done = all(p.get("status")
|
|
179
|
+
all_done = all(p.get("status") in ("completed", "cancelled") for p in prompts) and len(prompts) > 0
|
|
180
180
|
if all_done and not data.get("completedAt"):
|
|
181
181
|
data["completedAt"] = now
|
|
182
182
|
|
|
@@ -51,9 +51,14 @@ def atomic_write(path, obj):
|
|
|
51
51
|
except OSError: pass
|
|
52
52
|
raise
|
|
53
53
|
|
|
54
|
-
def close_session(path, now):
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
def close_session(path, now, data=None):
|
|
55
|
+
if data is None:
|
|
56
|
+
with open(path, "r") as f:
|
|
57
|
+
data = json.load(f)
|
|
58
|
+
for p in data.get("prompts", []):
|
|
59
|
+
if p.get("status") in ("pending", "running"):
|
|
60
|
+
p["status"] = "cancelled"
|
|
61
|
+
p["completedAt"] = now
|
|
57
62
|
data["closedAt"] = now
|
|
58
63
|
data["lastActivity"] = now
|
|
59
64
|
atomic_write(path, data)
|
|
@@ -80,10 +85,7 @@ for project_dir in glob.glob(os.path.join(queues_base, "*")):
|
|
|
80
85
|
data = json.load(f)
|
|
81
86
|
if data.get("closedAt") is not None:
|
|
82
87
|
continue
|
|
83
|
-
|
|
84
|
-
if has_pending:
|
|
85
|
-
continue
|
|
86
|
-
close_session(path, now)
|
|
88
|
+
close_session(path, now, data)
|
|
87
89
|
except (json.JSONDecodeError, IOError, OSError):
|
|
88
90
|
continue
|
|
89
91
|
|
|
@@ -3,8 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import type { SessionQueue, Prompt, PromptStatus, SessionStatus, QueueStatus, ProjectView, SessionWithStatus } from '../types/queue.ts';
|
|
4
4
|
|
|
5
5
|
export const SESSION_ACTIVE_TIMEOUT_MS = 60_000;
|
|
6
|
-
export const
|
|
7
|
-
export const SESSION_ABANDONED_TIMEOUT_MS = 24 * 60 * 60_000; // 24h safety net
|
|
6
|
+
export const SESSION_ABANDONED_TIMEOUT_MS = 24 * 60 * 60_000;
|
|
8
7
|
const LOCK_STALE_MS = 10_000;
|
|
9
8
|
|
|
10
9
|
function acquireLockSync(lockPath: string): void {
|
|
@@ -92,9 +91,6 @@ export function withComputedStatus(session: SessionQueue): SessionQueue & { stat
|
|
|
92
91
|
export function isSessionVisible(session: SessionQueue, now: number = Date.now()): boolean {
|
|
93
92
|
if (hasPendingWork(session)) return true;
|
|
94
93
|
if (session.closedAt != null) return false;
|
|
95
|
-
if (session.prompts.length === 0 && msSinceLastActivity(session, now) > SESSION_EMPTY_IDLE_TIMEOUT_MS) {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
94
|
const msSinceStart = now - new Date(session.startedAt).getTime();
|
|
99
95
|
return msSinceStart <= SESSION_ABANDONED_TIMEOUT_MS;
|
|
100
96
|
}
|
|
@@ -123,7 +119,7 @@ export function loadProjectView(queuesDir: string, project: string): ProjectView
|
|
|
123
119
|
|
|
124
120
|
const hasPrompts = sessions.some(s => s.prompts.length > 0);
|
|
125
121
|
const allCompleted = hasPrompts && sessions.every(s =>
|
|
126
|
-
s.prompts.length > 0 && s.prompts.every(p => p.status === 'completed')
|
|
122
|
+
s.prompts.length > 0 && s.prompts.every(p => p.status === 'completed' || p.status === 'cancelled')
|
|
127
123
|
);
|
|
128
124
|
const queueStatus: QueueStatus = allCompleted ? 'completed' : hasPrompts ? 'active' : 'empty';
|
|
129
125
|
|
|
@@ -178,7 +174,7 @@ export function updatePrompt(
|
|
|
178
174
|
}
|
|
179
175
|
if (updates.status !== undefined) {
|
|
180
176
|
session.prompts[idx].status = updates.status;
|
|
181
|
-
if (updates.status === 'completed') {
|
|
177
|
+
if (updates.status === 'completed' || updates.status === 'cancelled') {
|
|
182
178
|
session.prompts[idx].completedAt = new Date().toISOString();
|
|
183
179
|
}
|
|
184
180
|
}
|
|
@@ -32,6 +32,11 @@ const STATUS_STYLES: Record<Prompt['status'], { color: string; badge: string; la
|
|
|
32
32
|
badge: 'bg-[var(--color-completed)]/15 text-[var(--color-completed)]',
|
|
33
33
|
label: 'completed',
|
|
34
34
|
},
|
|
35
|
+
cancelled: {
|
|
36
|
+
color: 'var(--color-cancelled)',
|
|
37
|
+
badge: 'bg-[var(--color-cancelled)]/15 text-[var(--color-cancelled)]',
|
|
38
|
+
label: 'cancelled',
|
|
39
|
+
},
|
|
35
40
|
};
|
|
36
41
|
|
|
37
42
|
export function PromptCard({
|
|
@@ -52,7 +57,7 @@ export function PromptCard({
|
|
|
52
57
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
53
58
|
|
|
54
59
|
const styles = STATUS_STYLES[prompt.status];
|
|
55
|
-
const
|
|
60
|
+
const isDone = prompt.status === 'completed' || prompt.status === 'cancelled';
|
|
56
61
|
const isPending = prompt.status === 'pending';
|
|
57
62
|
const isRunning = prompt.status === 'running';
|
|
58
63
|
|
|
@@ -167,7 +172,7 @@ export function PromptCard({
|
|
|
167
172
|
className={[
|
|
168
173
|
'flex gap-0 bg-white/5 backdrop-blur-sm border border-white/10 rounded-lg overflow-hidden',
|
|
169
174
|
'transition-all duration-150',
|
|
170
|
-
|
|
175
|
+
isDone ? 'opacity-60' : '',
|
|
171
176
|
isPending && !editing ? 'cursor-pointer hover:border-white/20 hover:bg-white/8' : '',
|
|
172
177
|
isRunning ? 'border-l-0' : '',
|
|
173
178
|
isDragging ? 'opacity-40 scale-[0.98]' : '',
|
|
@@ -46,8 +46,8 @@ export function SessionSection({ session, project, onMutate, defaultExpanded = t
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const activePrompts = session.prompts.filter(p => p.status !== 'completed');
|
|
50
|
-
const
|
|
49
|
+
const activePrompts = session.prompts.filter(p => p.status !== 'completed' && p.status !== 'cancelled');
|
|
50
|
+
const donePrompts = session.prompts.filter(p => p.status === 'completed' || p.status === 'cancelled').reverse();
|
|
51
51
|
const pendingCount = session.prompts.filter(p => p.status === 'pending').length;
|
|
52
52
|
|
|
53
53
|
const displayName = session.sessionName || '(session)';
|
|
@@ -168,7 +168,7 @@ export function SessionSection({ session, project, onMutate, defaultExpanded = t
|
|
|
168
168
|
{/* Session content */}
|
|
169
169
|
{expanded && (
|
|
170
170
|
<div className="px-4 pb-4 space-y-2">
|
|
171
|
-
{activePrompts.length === 0 &&
|
|
171
|
+
{activePrompts.length === 0 && donePrompts.length === 0 && (
|
|
172
172
|
<p className="text-xs text-[var(--color-muted)] py-2">No prompts yet</p>
|
|
173
173
|
)}
|
|
174
174
|
|
|
@@ -180,9 +180,9 @@ export function SessionSection({ session, project, onMutate, defaultExpanded = t
|
|
|
180
180
|
|
|
181
181
|
<AddPromptForm project={project} sessionId={session.sessionId} onAdded={onMutate} />
|
|
182
182
|
|
|
183
|
-
{
|
|
183
|
+
{donePrompts.length > 0 && (
|
|
184
184
|
<div className="pt-1 space-y-2 opacity-60" role="list" aria-label="Completed prompts">
|
|
185
|
-
{renderPromptList(
|
|
185
|
+
{renderPromptList(donePrompts, false)}
|
|
186
186
|
</div>
|
|
187
187
|
)}
|
|
188
188
|
</div>
|
package/src/index.css
CHANGED
package/src/types/queue.ts
CHANGED
package/vite-plugin-api.ts
CHANGED
|
@@ -269,7 +269,7 @@ async function handleApi(
|
|
|
269
269
|
updates.text = body.text as string;
|
|
270
270
|
}
|
|
271
271
|
if (body.status !== undefined) {
|
|
272
|
-
const validStatuses: PromptStatus[] = ['pending', 'running', 'completed'];
|
|
272
|
+
const validStatuses: PromptStatus[] = ['pending', 'running', 'completed', 'cancelled'];
|
|
273
273
|
if (!validStatuses.includes(body.status as PromptStatus)) {
|
|
274
274
|
return jsonError(res, 400, `Invalid status. Must be one of: ${validStatuses.join(', ')}`);
|
|
275
275
|
}
|