@jxtools/promptline 1.3.14 → 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 +2 -2
- 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
|
|
|
@@ -119,7 +119,7 @@ export function loadProjectView(queuesDir: string, project: string): ProjectView
|
|
|
119
119
|
|
|
120
120
|
const hasPrompts = sessions.some(s => s.prompts.length > 0);
|
|
121
121
|
const allCompleted = hasPrompts && sessions.every(s =>
|
|
122
|
-
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')
|
|
123
123
|
);
|
|
124
124
|
const queueStatus: QueueStatus = allCompleted ? 'completed' : hasPrompts ? 'active' : 'empty';
|
|
125
125
|
|
|
@@ -174,7 +174,7 @@ export function updatePrompt(
|
|
|
174
174
|
}
|
|
175
175
|
if (updates.status !== undefined) {
|
|
176
176
|
session.prompts[idx].status = updates.status;
|
|
177
|
-
if (updates.status === 'completed') {
|
|
177
|
+
if (updates.status === 'completed' || updates.status === 'cancelled') {
|
|
178
178
|
session.prompts[idx].completedAt = new Date().toISOString();
|
|
179
179
|
}
|
|
180
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
|
}
|