@jxtools/promptline 1.0.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxtools/promptline",
3
- "version": "1.0.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -18,6 +18,10 @@
18
18
  "index.html",
19
19
  "README.md"
20
20
  ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/juancruzrossi/promptline"
24
+ },
21
25
  "publishConfig": {
22
26
  "access": "public"
23
27
  },
@@ -30,18 +30,21 @@ if [ -z "$CWD" ]; then
30
30
  exit 0
31
31
  fi
32
32
 
33
- # --- Derive project name and session file path ---
34
- PROJECT=$(basename "$CWD")
35
- QUEUE_DIR="$HOME/.promptline/queues/$PROJECT"
36
- QUEUE_FILE="$QUEUE_DIR/$SESSION_ID.json"
37
-
38
- export QUEUE_FILE SESSION_ID CWD PROJECT TRANSCRIPT_PATH STOP_HOOK_ACTIVE
39
-
40
- # No session file -> nothing to do (SessionStart hook handles registration)
41
- if [ ! -f "$QUEUE_FILE" ]; then
33
+ # --- Search for existing session across all projects ---
34
+ QUEUES_BASE="$HOME/.promptline/queues"
35
+ EXISTING=$(find "$QUEUES_BASE" -maxdepth 2 -name "$SESSION_ID.json" -print -quit 2>/dev/null || true)
36
+
37
+ if [ -n "$EXISTING" ]; then
38
+ QUEUE_FILE="$EXISTING"
39
+ PROJECT=$(basename "$(dirname "$EXISTING")")
40
+ QUEUE_DIR="$(dirname "$EXISTING")"
41
+ else
42
+ # No session file -> nothing to do
42
43
  exit 0
43
44
  fi
44
45
 
46
+ export QUEUE_FILE SESSION_ID CWD PROJECT TRANSCRIPT_PATH STOP_HOOK_ACTIVE
47
+
45
48
  # --- Process queue with python3 ---
46
49
  RESULT=$(python3 << 'PYEOF'
47
50
  import json
@@ -21,8 +21,17 @@ if [ -z "$CWD" ] || [ -z "$SESSION_ID" ]; then
21
21
  exit 0
22
22
  fi
23
23
 
24
- PROJECT=$(basename "$CWD")
25
- QUEUE_FILE="$HOME/.promptline/queues/$PROJECT/$SESSION_ID.json"
24
+ # Search for existing session across all projects
25
+ QUEUES_BASE="$HOME/.promptline/queues"
26
+ EXISTING=$(find "$QUEUES_BASE" -maxdepth 2 -name "$SESSION_ID.json" -print -quit 2>/dev/null || true)
27
+
28
+ if [ -n "$EXISTING" ]; then
29
+ QUEUE_FILE="$EXISTING"
30
+ else
31
+ # No session to close
32
+ exit 0
33
+ fi
34
+
26
35
  export QUEUE_FILE
27
36
 
28
37
  python3 << 'PYEOF'
@@ -24,11 +24,20 @@ if [ -z "$CWD" ] || [ -z "$SESSION_ID" ]; then
24
24
  exit 0
25
25
  fi
26
26
 
27
- PROJECT=$(basename "$CWD")
28
- QUEUE_DIR="$HOME/.promptline/queues/$PROJECT"
29
- QUEUE_FILE="$QUEUE_DIR/$SESSION_ID.json"
27
+ # Search for existing session across all projects
28
+ QUEUES_BASE="$HOME/.promptline/queues"
29
+ EXISTING=$(find "$QUEUES_BASE" -maxdepth 2 -name "$SESSION_ID.json" -print -quit 2>/dev/null || true)
30
30
 
31
- mkdir -p "$QUEUE_DIR"
31
+ if [ -n "$EXISTING" ]; then
32
+ QUEUE_FILE="$EXISTING"
33
+ PROJECT=$(basename "$(dirname "$EXISTING")")
34
+ QUEUE_DIR="$(dirname "$EXISTING")"
35
+ else
36
+ PROJECT=$(basename "$CWD")
37
+ QUEUE_DIR="$QUEUES_BASE/$PROJECT"
38
+ QUEUE_FILE="$QUEUE_DIR/$SESSION_ID.json"
39
+ mkdir -p "$QUEUE_DIR"
40
+ fi
32
41
 
33
42
  export QUEUE_FILE SESSION_ID CWD PROJECT TRANSCRIPT_PATH
34
43
 
@@ -3,7 +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 SESSION_VISIBLE_TIMEOUT_MS = 5 * SESSION_ACTIVE_TIMEOUT_MS;
6
+ export const SESSION_ABANDONED_TIMEOUT_MS = 24 * 60 * 60_000; // 24h safety net
7
7
 
8
8
  export function ensureProjectDir(queuesDir: string, project: string): void {
9
9
  mkdirSync(join(queuesDir, project), { recursive: true });
@@ -52,7 +52,9 @@ export function withComputedStatus(session: SessionQueue): SessionQueue & { stat
52
52
  export function isSessionVisible(session: SessionQueue, now: number = Date.now()): boolean {
53
53
  if (hasPendingWork(session)) return true;
54
54
  if (session.closedAt != null) return false;
55
- return msSinceLastActivity(session, now) <= SESSION_VISIBLE_TIMEOUT_MS;
55
+ if (!session.sessionName) return false;
56
+ const msSinceStart = now - new Date(session.startedAt).getTime();
57
+ return msSinceStart <= SESSION_ABANDONED_TIMEOUT_MS;
56
58
  }
57
59
 
58
60
  export function loadProjectView(queuesDir: string, project: string): ProjectView | null {
@@ -206,19 +206,22 @@ export function PromptCard({
206
206
  {styles.label}
207
207
  </span>
208
208
 
209
- {/* Action buttons */}
209
+ {/* Delete button */}
210
210
  {!editing && !isRunning && (
211
211
  <button
212
212
  type="button"
213
213
  onClick={handleDelete}
214
214
  className={[
215
- 'text-xs px-1.5 py-0.5 rounded text-[var(--color-muted)] cursor-pointer opacity-0 group-hover:opacity-100',
216
- 'hover:text-red-400 hover:bg-red-400/10',
215
+ 'p-1 rounded cursor-pointer',
216
+ 'text-[var(--color-muted)]/40 hover:text-red-400 hover:bg-red-400/10',
217
217
  'transition-all duration-100 focus:outline-none',
218
218
  ].join(' ')}
219
219
  aria-label="Delete prompt"
220
220
  >
221
-
221
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
222
+ <polyline points="3 6 5 6 21 6" />
223
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
224
+ </svg>
222
225
  </button>
223
226
  )}
224
227
  </div>
@@ -34,6 +34,16 @@ export function SessionSection({ session, project, onMutate, defaultExpanded = t
34
34
  const [draggingId, setDraggingId] = useState<string | null>(null);
35
35
  const dragSourceRef = useRef<string | null>(null);
36
36
 
37
+ async function handleDeleteSession(e: React.MouseEvent) {
38
+ e.stopPropagation();
39
+ try {
40
+ await api.deleteSession(project, session.sessionId);
41
+ onMutate();
42
+ } catch {
43
+ // Silent fail
44
+ }
45
+ }
46
+
37
47
  const activePrompts = session.prompts.filter(p => p.status !== 'completed');
38
48
  const completedPrompts = session.prompts.filter(p => p.status === 'completed').reverse();
39
49
  const pendingCount = session.prompts.filter(p => p.status === 'pending').length;
@@ -107,11 +117,12 @@ export function SessionSection({ session, project, onMutate, defaultExpanded = t
107
117
  return (
108
118
  <div className="border border-[var(--color-border)] rounded-lg overflow-hidden bg-white/[0.02]">
109
119
  {/* Session header */}
120
+ <div className="relative group">
110
121
  <button
111
122
  type="button"
112
123
  onClick={() => setExpanded(v => !v)}
113
124
  className={[
114
- 'w-full flex items-center gap-3 px-4 py-3 text-left cursor-pointer',
125
+ 'w-full flex items-center gap-3 pl-4 pr-10 py-3 text-left cursor-pointer',
115
126
  'hover:bg-white/5 transition-colors duration-150 focus:outline-none',
116
127
  ].join(' ')}
117
128
  aria-expanded={expanded}
@@ -136,6 +147,22 @@ export function SessionSection({ session, project, onMutate, defaultExpanded = t
136
147
 
137
148
  </span>
138
149
  </button>
150
+ <button
151
+ type="button"
152
+ onClick={handleDeleteSession}
153
+ className={[
154
+ 'absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded cursor-pointer',
155
+ 'text-[var(--color-muted)]/40 hover:text-red-400 hover:bg-red-400/10',
156
+ 'transition-all duration-100 focus:outline-none',
157
+ ].join(' ')}
158
+ aria-label="Delete session"
159
+ >
160
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
161
+ <polyline points="3 6 5 6 21 6" />
162
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
163
+ </svg>
164
+ </button>
165
+ </div>
139
166
 
140
167
  {/* Session content */}
141
168
  {expanded && (