@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxtools/promptline",
3
- "version": "1.3.14",
3
+ "version": "1.3.15",
4
4
  "type": "module",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -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") == "completed" for p in prompts) and len(prompts) > 0
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
- with open(path, "r") as f:
56
- data = json.load(f)
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
- has_pending = any(p.get("status") in ("pending", "running") for p in data.get("prompts", []))
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 isCompleted = prompt.status === 'completed';
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
- isCompleted ? 'opacity-60' : '',
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 completedPrompts = session.prompts.filter(p => p.status === 'completed').reverse();
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 && completedPrompts.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
- {completedPrompts.length > 0 && (
183
+ {donePrompts.length > 0 && (
184
184
  <div className="pt-1 space-y-2 opacity-60" role="list" aria-label="Completed prompts">
185
- {renderPromptList(completedPrompts, false)}
185
+ {renderPromptList(donePrompts, false)}
186
186
  </div>
187
187
  )}
188
188
  </div>
package/src/index.css CHANGED
@@ -11,6 +11,7 @@
11
11
  --color-pending: #60a5fa;
12
12
  --color-idle: #f472b6;
13
13
  --color-completed: #34d399;
14
+ --color-cancelled: #f87171;
14
15
  }
15
16
 
16
17
  html {
@@ -1,4 +1,4 @@
1
- export type PromptStatus = 'pending' | 'running' | 'completed';
1
+ export type PromptStatus = 'pending' | 'running' | 'completed' | 'cancelled';
2
2
  export type SessionStatus = 'active' | 'idle';
3
3
  export type QueueStatus = 'active' | 'completed' | 'empty';
4
4
 
@@ -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
  }