onako 0.4.3__py3-none-any.whl → 0.4.4__py3-none-any.whl
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.
- onako/__init__.py +1 -1
- onako/cli.py +21 -7
- onako/server.py +18 -5
- onako/static/index.html +321 -22
- onako/tmux_orchestrator.py +79 -17
- {onako-0.4.3.dist-info → onako-0.4.4.dist-info}/METADATA +1 -1
- onako-0.4.4.dist-info/RECORD +10 -0
- onako-0.4.3.dist-info/RECORD +0 -10
- {onako-0.4.3.dist-info → onako-0.4.4.dist-info}/WHEEL +0 -0
- {onako-0.4.3.dist-info → onako-0.4.4.dist-info}/entry_points.txt +0 -0
- {onako-0.4.3.dist-info → onako-0.4.4.dist-info}/top_level.txt +0 -0
onako/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.4.
|
|
1
|
+
__version__ = "0.4.4"
|
onako/cli.py
CHANGED
|
@@ -10,6 +10,7 @@ import click
|
|
|
10
10
|
ONAKO_DIR = Path.home() / ".onako"
|
|
11
11
|
LOG_DIR = ONAKO_DIR / "logs"
|
|
12
12
|
PID_FILE = ONAKO_DIR / "onako.pid"
|
|
13
|
+
DEV_MARKER = ONAKO_DIR / "onako.dev"
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@click.group(invoke_without_command=True)
|
|
@@ -18,8 +19,9 @@ PID_FILE = ONAKO_DIR / "onako.pid"
|
|
|
18
19
|
@click.option("--session", default="onako", help="tmux session name (auto-detected if inside tmux).")
|
|
19
20
|
@click.option("--dir", "working_dir", default=None, type=click.Path(exists=True), help="Working directory for tasks (default: current directory).")
|
|
20
21
|
@click.option("--dangerously-skip-permissions", "skip_permissions", is_flag=True, default=False, help="Pass --dangerously-skip-permissions to all Claude Code tasks.")
|
|
22
|
+
@click.option("--no-attach", is_flag=True, default=False, help="Start the server without attaching to tmux.")
|
|
21
23
|
@click.pass_context
|
|
22
|
-
def main(ctx, host, port, session, working_dir, skip_permissions):
|
|
24
|
+
def main(ctx, host, port, session, working_dir, skip_permissions, no_attach):
|
|
23
25
|
"""Onako — Dispatch and monitor Claude Code tasks from your phone."""
|
|
24
26
|
if ctx.invoked_subcommand is not None:
|
|
25
27
|
return
|
|
@@ -65,7 +67,7 @@ def main(ctx, host, port, session, working_dir, skip_permissions):
|
|
|
65
67
|
capture_output=True,
|
|
66
68
|
)
|
|
67
69
|
|
|
68
|
-
if not inside_tmux:
|
|
70
|
+
if not inside_tmux and not no_attach:
|
|
69
71
|
os.execvp("tmux", ["tmux", "attach-session", "-t", session])
|
|
70
72
|
|
|
71
73
|
|
|
@@ -115,11 +117,13 @@ def stop():
|
|
|
115
117
|
try:
|
|
116
118
|
pid = int(PID_FILE.read_text().strip())
|
|
117
119
|
os.kill(pid, 15) # SIGTERM
|
|
118
|
-
|
|
120
|
+
dev = " (dev)" if DEV_MARKER.exists() else ""
|
|
121
|
+
click.echo(f"Onako server stopped{dev} (pid {pid}).")
|
|
119
122
|
stopped = True
|
|
120
123
|
except (ValueError, ProcessLookupError):
|
|
121
124
|
click.echo("Stale pid file found, cleaning up.")
|
|
122
125
|
PID_FILE.unlink(missing_ok=True)
|
|
126
|
+
DEV_MARKER.unlink(missing_ok=True)
|
|
123
127
|
|
|
124
128
|
if not stopped:
|
|
125
129
|
click.echo("Onako service is not running.")
|
|
@@ -146,10 +150,12 @@ def purge(session):
|
|
|
146
150
|
try:
|
|
147
151
|
pid = int(PID_FILE.read_text().strip())
|
|
148
152
|
os.kill(pid, 15)
|
|
149
|
-
|
|
153
|
+
dev = " (dev)" if DEV_MARKER.exists() else ""
|
|
154
|
+
click.echo(f"Stopped onako server{dev} (pid {pid}).")
|
|
150
155
|
except (ValueError, ProcessLookupError):
|
|
151
156
|
pass
|
|
152
157
|
PID_FILE.unlink(missing_ok=True)
|
|
158
|
+
DEV_MARKER.unlink(missing_ok=True)
|
|
153
159
|
|
|
154
160
|
# Kill the tmux session
|
|
155
161
|
result = subprocess.run(
|
|
@@ -182,7 +188,8 @@ def status():
|
|
|
182
188
|
r = urllib.request.urlopen("http://127.0.0.1:8787/health", timeout=2)
|
|
183
189
|
data = r.read().decode()
|
|
184
190
|
if '"ok"' in data:
|
|
185
|
-
|
|
191
|
+
mode = " (dev)" if DEV_MARKER.exists() else ""
|
|
192
|
+
click.echo(f"Onako server: running{mode}")
|
|
186
193
|
click.echo(" URL: http://127.0.0.1:8787")
|
|
187
194
|
else:
|
|
188
195
|
click.echo("Onako server: not responding correctly")
|
|
@@ -209,7 +216,8 @@ def _start_server(host, port, session, working_dir, skip_permissions=False):
|
|
|
209
216
|
Returns True if the server was started or is already running.
|
|
210
217
|
"""
|
|
211
218
|
if _is_server_running():
|
|
212
|
-
|
|
219
|
+
dev = " (dev)" if DEV_MARKER.exists() else ""
|
|
220
|
+
click.echo(f"Onako server already running{dev} (pid {PID_FILE.read_text().strip()})")
|
|
213
221
|
return True
|
|
214
222
|
|
|
215
223
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -234,9 +242,15 @@ def _start_server(host, port, session, working_dir, skip_permissions=False):
|
|
|
234
242
|
PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
235
243
|
PID_FILE.write_text(str(proc.pid))
|
|
236
244
|
|
|
245
|
+
if os.environ.get("ONAKO_DEV"):
|
|
246
|
+
DEV_MARKER.write_text("")
|
|
247
|
+
else:
|
|
248
|
+
DEV_MARKER.unlink(missing_ok=True)
|
|
249
|
+
|
|
250
|
+
dev = " (dev)" if DEV_MARKER.exists() else ""
|
|
237
251
|
local_ip = _get_local_ip()
|
|
238
252
|
banner = [
|
|
239
|
-
f"Onako server started (pid {proc.pid})",
|
|
253
|
+
f"Onako server started{dev} (pid {proc.pid})",
|
|
240
254
|
f" Dashboard: http://{host}:{port}",
|
|
241
255
|
]
|
|
242
256
|
if local_ip:
|
onako/server.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shlex
|
|
3
|
+
import uuid
|
|
3
4
|
|
|
4
5
|
from fastapi import FastAPI, HTTPException
|
|
5
6
|
from fastapi.responses import FileResponse
|
|
@@ -37,11 +38,12 @@ def health():
|
|
|
37
38
|
@app.post("/tasks")
|
|
38
39
|
def create_task(req: CreateTaskRequest):
|
|
39
40
|
skip = req.skip_permissions if req.skip_permissions is not None else skip_permissions_default
|
|
41
|
+
claude_session_id = str(uuid.uuid4())
|
|
40
42
|
if skip:
|
|
41
|
-
command = f"claude --dangerously-skip-permissions {shlex.quote(req.prompt)}"
|
|
43
|
+
command = f"claude --dangerously-skip-permissions --session-id {claude_session_id} {shlex.quote(req.prompt)}"
|
|
42
44
|
else:
|
|
43
|
-
command = f"claude {shlex.quote(req.prompt)}"
|
|
44
|
-
task = orch.create_task(command, working_dir=req.working_dir, prompt=req.prompt)
|
|
45
|
+
command = f"claude --session-id {claude_session_id} {shlex.quote(req.prompt)}"
|
|
46
|
+
task = orch.create_task(command, working_dir=req.working_dir, prompt=req.prompt, claude_session_id=claude_session_id)
|
|
45
47
|
return task
|
|
46
48
|
|
|
47
49
|
|
|
@@ -84,12 +86,23 @@ def interrupt_task(task_id: str):
|
|
|
84
86
|
return {"status": "interrupted"}
|
|
85
87
|
|
|
86
88
|
|
|
89
|
+
@app.post("/tasks/{task_id}/resume")
|
|
90
|
+
def resume_task(task_id: str):
|
|
91
|
+
if task_id not in orch.tasks:
|
|
92
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
93
|
+
try:
|
|
94
|
+
new_task = orch.resume_task(task_id, skip_permissions=skip_permissions_default)
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
97
|
+
return new_task
|
|
98
|
+
|
|
99
|
+
|
|
87
100
|
@app.delete("/tasks/{task_id}")
|
|
88
101
|
def delete_task(task_id: str):
|
|
89
102
|
if task_id not in orch.tasks:
|
|
90
103
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
91
|
-
if
|
|
92
|
-
raise HTTPException(status_code=400, detail="Cannot kill the
|
|
104
|
+
if orch.window_count() <= 1:
|
|
105
|
+
raise HTTPException(status_code=400, detail="Cannot kill the last window")
|
|
93
106
|
orch.kill_task(task_id)
|
|
94
107
|
return {"status": "deleted"}
|
|
95
108
|
|
onako/static/index.html
CHANGED
|
@@ -33,6 +33,33 @@
|
|
|
33
33
|
cursor: pointer;
|
|
34
34
|
font-size: 14px;
|
|
35
35
|
}
|
|
36
|
+
.task-controls {
|
|
37
|
+
padding: 8px;
|
|
38
|
+
display: flex;
|
|
39
|
+
gap: 6px;
|
|
40
|
+
border-bottom: 1px solid #2a2a2a;
|
|
41
|
+
}
|
|
42
|
+
.task-controls input {
|
|
43
|
+
flex: 1;
|
|
44
|
+
min-width: 0;
|
|
45
|
+
background: #2a2a2a;
|
|
46
|
+
border: 1px solid #333;
|
|
47
|
+
color: #e0e0e0;
|
|
48
|
+
padding: 6px 10px;
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
font-size: 13px;
|
|
51
|
+
-webkit-appearance: none;
|
|
52
|
+
}
|
|
53
|
+
.task-controls select {
|
|
54
|
+
background: #2a2a2a;
|
|
55
|
+
border: 1px solid #333;
|
|
56
|
+
color: #e0e0e0;
|
|
57
|
+
padding: 6px 8px;
|
|
58
|
+
border-radius: 6px;
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
-webkit-appearance: none;
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
}
|
|
36
63
|
.task-list { padding: 8px; }
|
|
37
64
|
.task-item {
|
|
38
65
|
padding: 12px;
|
|
@@ -72,6 +99,40 @@
|
|
|
72
99
|
padding: 48px 16px;
|
|
73
100
|
font-size: 14px;
|
|
74
101
|
}
|
|
102
|
+
.section-header {
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 6px;
|
|
106
|
+
padding: 10px 12px 6px;
|
|
107
|
+
font-size: 11px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
color: #666;
|
|
110
|
+
text-transform: uppercase;
|
|
111
|
+
letter-spacing: 0.5px;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
user-select: none;
|
|
114
|
+
}
|
|
115
|
+
.section-header .chevron {
|
|
116
|
+
transition: transform 0.15s;
|
|
117
|
+
font-size: 10px;
|
|
118
|
+
}
|
|
119
|
+
.section-header.collapsed .chevron {
|
|
120
|
+
transform: rotate(-90deg);
|
|
121
|
+
}
|
|
122
|
+
.recent-tasks.collapsed { display: none; }
|
|
123
|
+
.task-item.done { opacity: 0.6; }
|
|
124
|
+
.resume-btn {
|
|
125
|
+
display: inline-block;
|
|
126
|
+
background: #3b82f6;
|
|
127
|
+
color: white;
|
|
128
|
+
border: none;
|
|
129
|
+
padding: 3px 10px;
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
font-size: 11px;
|
|
133
|
+
margin-left: 8px;
|
|
134
|
+
vertical-align: middle;
|
|
135
|
+
}
|
|
75
136
|
#connection-banner {
|
|
76
137
|
display: none;
|
|
77
138
|
background: #ef4444;
|
|
@@ -127,16 +188,38 @@
|
|
|
127
188
|
font-size: 12px;
|
|
128
189
|
}
|
|
129
190
|
#kill-btn.hidden { display: none; }
|
|
191
|
+
#resume-detail-btn {
|
|
192
|
+
background: #3b82f6;
|
|
193
|
+
color: white;
|
|
194
|
+
border: none;
|
|
195
|
+
padding: 6px 12px;
|
|
196
|
+
border-radius: 6px;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
font-size: 12px;
|
|
199
|
+
}
|
|
200
|
+
#resume-detail-btn.hidden { display: none; }
|
|
130
201
|
#output {
|
|
131
202
|
padding: 12px;
|
|
132
203
|
font-family: "SF Mono", "Menlo", "Monaco", monospace;
|
|
133
204
|
font-size: 12px;
|
|
134
|
-
|
|
135
|
-
word-break: break-all;
|
|
136
|
-
line-height: 1.4;
|
|
205
|
+
line-height: 1.5;
|
|
137
206
|
overflow-y: auto;
|
|
138
207
|
flex: 1;
|
|
139
208
|
}
|
|
209
|
+
.output-block {
|
|
210
|
+
white-space: pre-wrap;
|
|
211
|
+
word-break: break-word;
|
|
212
|
+
padding: 4px 0;
|
|
213
|
+
}
|
|
214
|
+
.block-user {
|
|
215
|
+
border-left: 2px solid #555;
|
|
216
|
+
padding-left: 10px;
|
|
217
|
+
margin-left: 2px;
|
|
218
|
+
color: #999;
|
|
219
|
+
}
|
|
220
|
+
.block-plain {
|
|
221
|
+
color: #888;
|
|
222
|
+
}
|
|
140
223
|
#message-bar {
|
|
141
224
|
padding: 8px;
|
|
142
225
|
padding-bottom: max(8px, env(safe-area-inset-bottom));
|
|
@@ -271,6 +354,15 @@
|
|
|
271
354
|
<h1>Onako</h1>
|
|
272
355
|
<button id="new-task-btn">+ New Task</button>
|
|
273
356
|
</header>
|
|
357
|
+
<div class="task-controls">
|
|
358
|
+
<input id="search-input" type="text" placeholder="Search...">
|
|
359
|
+
<select id="sort-select">
|
|
360
|
+
<option value="active-desc">Active ↓</option>
|
|
361
|
+
<option value="active-asc">Active ↑</option>
|
|
362
|
+
<option value="started-desc">Started ↓</option>
|
|
363
|
+
<option value="started-asc">Started ↑</option>
|
|
364
|
+
</select>
|
|
365
|
+
</div>
|
|
274
366
|
<div class="task-list" id="task-list"></div>
|
|
275
367
|
</div>
|
|
276
368
|
|
|
@@ -280,6 +372,7 @@
|
|
|
280
372
|
<button id="back-btn">← Back</button>
|
|
281
373
|
<span id="detail-task-id"></span>
|
|
282
374
|
<div style="display:flex;gap:8px">
|
|
375
|
+
<button id="resume-detail-btn" class="hidden">Resume</button>
|
|
283
376
|
<button id="interrupt-btn">Interrupt</button>
|
|
284
377
|
<button id="kill-btn">Kill</button>
|
|
285
378
|
</div>
|
|
@@ -337,35 +430,90 @@
|
|
|
337
430
|
const res = await fetch(`${API}/health`);
|
|
338
431
|
const data = await res.json();
|
|
339
432
|
skipPermissionsDefault = data.skip_permissions || false;
|
|
340
|
-
|
|
433
|
+
const saved = localStorage.getItem('skipPermissions');
|
|
434
|
+
const initial = saved !== null ? saved === '1' : skipPermissionsDefault;
|
|
435
|
+
document.getElementById('skip-perms-input').checked = initial;
|
|
341
436
|
} catch (e) {}
|
|
342
437
|
}
|
|
343
438
|
|
|
439
|
+
let recentCollapsed = localStorage.getItem('recentCollapsed') === '1';
|
|
440
|
+
|
|
441
|
+
function renderTaskItem(t) {
|
|
442
|
+
const isDone = t.status === 'done';
|
|
443
|
+
const resumable = isDone && t.claude_session_id;
|
|
444
|
+
return `
|
|
445
|
+
<div class="task-item${currentTaskId === t.id ? ' selected' : ''}${isDone ? ' done' : ''}" onclick="showTask('${t.id}')">
|
|
446
|
+
<div class="task-id">${t.id}${t.origin === 'external' ? '<span class="origin-badge">external</span>' : ''}${resumable ? '<button class="resume-btn" onclick="event.stopPropagation();resumeTask(\''+t.id+'\')">Resume</button>' : ''}</div>
|
|
447
|
+
<div class="task-prompt">${escapeHtml(t.prompt)}</div>
|
|
448
|
+
<div class="task-meta">
|
|
449
|
+
<span class="status-${t.status}">${isDone ? 'done' : '✓'}</span>
|
|
450
|
+
${t.started_at ? ' · started ' + timeAgo(t.started_at) : ''}
|
|
451
|
+
${t.last_activity ? ' · active ' + timeAgo(t.last_activity) : ''}
|
|
452
|
+
</div>
|
|
453
|
+
</div>`;
|
|
454
|
+
}
|
|
455
|
+
|
|
344
456
|
async function loadTasks() {
|
|
345
457
|
try {
|
|
346
458
|
const res = await fetch(`${API}/tasks`);
|
|
347
|
-
const
|
|
459
|
+
const allTasks = await res.json();
|
|
460
|
+
allTasksCache = allTasks;
|
|
461
|
+
const query = document.getElementById('search-input').value.trim().toLowerCase();
|
|
462
|
+
let running = allTasks.filter(t => t.status === 'running');
|
|
463
|
+
let done = allTasks.filter(t => t.status === 'done' && t.claude_session_id);
|
|
464
|
+
if (query) {
|
|
465
|
+
running = running.filter(t => t.prompt.toLowerCase().includes(query));
|
|
466
|
+
done = done.filter(t => t.prompt.toLowerCase().includes(query));
|
|
467
|
+
}
|
|
468
|
+
const sort = document.getElementById('sort-select').value;
|
|
469
|
+
const [field, dir] = sort.split('-');
|
|
470
|
+
const key = field === 'active' ? 'last_activity' : 'started_at';
|
|
471
|
+
const mul = dir === 'desc' ? 1 : -1;
|
|
472
|
+
const sortFn = (a, b) => {
|
|
473
|
+
const va = a[key] || '';
|
|
474
|
+
const vb = b[key] || '';
|
|
475
|
+
return va < vb ? mul : va > vb ? -mul : 0;
|
|
476
|
+
};
|
|
477
|
+
running.sort(sortFn);
|
|
478
|
+
done.sort((a, b) => {
|
|
479
|
+
const va = a.last_activity || a.started_at || '';
|
|
480
|
+
const vb = b.last_activity || b.started_at || '';
|
|
481
|
+
return va < vb ? 1 : va > vb ? -1 : 0;
|
|
482
|
+
});
|
|
483
|
+
// Limit recent to last 20
|
|
484
|
+
done = done.slice(0, 20);
|
|
348
485
|
const list = document.getElementById('task-list');
|
|
349
|
-
|
|
350
|
-
|
|
486
|
+
let html = '';
|
|
487
|
+
if (running.length === 0 && done.length === 0) {
|
|
488
|
+
html = '<div class="empty-state">No tasks</div>';
|
|
351
489
|
} else {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
490
|
+
if (running.length > 0) {
|
|
491
|
+
html += running.map(renderTaskItem).join('');
|
|
492
|
+
} else {
|
|
493
|
+
html += '<div class="empty-state">No tasks running</div>';
|
|
494
|
+
}
|
|
495
|
+
if (done.length > 0) {
|
|
496
|
+
html += `<div class="section-header${recentCollapsed ? ' collapsed' : ''}" onclick="toggleRecent()"><span class="chevron">▼</span> Recent (${done.length})</div>`;
|
|
497
|
+
html += `<div class="recent-tasks${recentCollapsed ? ' collapsed' : ''}">`;
|
|
498
|
+
html += done.map(renderTaskItem).join('');
|
|
499
|
+
html += '</div>';
|
|
500
|
+
}
|
|
362
501
|
}
|
|
502
|
+
list.innerHTML = html;
|
|
363
503
|
showConnectionError(false);
|
|
364
504
|
} catch (e) {
|
|
365
505
|
showConnectionError(true);
|
|
366
506
|
}
|
|
367
507
|
}
|
|
368
508
|
|
|
509
|
+
function toggleRecent() {
|
|
510
|
+
recentCollapsed = !recentCollapsed;
|
|
511
|
+
localStorage.setItem('recentCollapsed', recentCollapsed ? '1' : '0');
|
|
512
|
+
loadTasks();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let allTasksCache = [];
|
|
516
|
+
|
|
369
517
|
async function showTask(id) {
|
|
370
518
|
currentTaskId = id;
|
|
371
519
|
currentTaskStatus = null;
|
|
@@ -382,16 +530,23 @@
|
|
|
382
530
|
document.getElementById('list-view').classList.add('hidden');
|
|
383
531
|
document.getElementById('detail-view').classList.add('active');
|
|
384
532
|
}
|
|
385
|
-
if
|
|
533
|
+
// Check if task is already done
|
|
534
|
+
const taskData = allTasksCache.find(t => t.id === id);
|
|
535
|
+
if (taskData && taskData.status === 'done') {
|
|
536
|
+
currentTaskStatus = 'done';
|
|
386
537
|
document.getElementById('interrupt-btn').classList.add('hidden');
|
|
387
538
|
document.getElementById('kill-btn').classList.add('hidden');
|
|
539
|
+
document.getElementById('resume-detail-btn').classList.toggle('hidden', !taskData.claude_session_id);
|
|
388
540
|
} else {
|
|
389
541
|
document.getElementById('interrupt-btn').classList.remove('hidden');
|
|
390
542
|
document.getElementById('kill-btn').classList.remove('hidden');
|
|
543
|
+
document.getElementById('resume-detail-btn').classList.add('hidden');
|
|
391
544
|
}
|
|
392
545
|
if (pollInterval) clearInterval(pollInterval);
|
|
393
546
|
await refreshOutput();
|
|
394
|
-
|
|
547
|
+
if (currentTaskStatus !== 'done') {
|
|
548
|
+
pollInterval = setInterval(refreshOutput, 3000);
|
|
549
|
+
}
|
|
395
550
|
}
|
|
396
551
|
|
|
397
552
|
async function refreshOutput() {
|
|
@@ -402,7 +557,11 @@
|
|
|
402
557
|
const data = await res.json();
|
|
403
558
|
const el = document.getElementById('output');
|
|
404
559
|
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
|
|
405
|
-
|
|
560
|
+
if (data.output) {
|
|
561
|
+
el.innerHTML = renderOutput(data.output);
|
|
562
|
+
} else {
|
|
563
|
+
el.textContent = '(no output yet)';
|
|
564
|
+
}
|
|
406
565
|
if (wasAtBottom) el.scrollTop = el.scrollHeight;
|
|
407
566
|
showConnectionError(false);
|
|
408
567
|
|
|
@@ -411,10 +570,12 @@
|
|
|
411
570
|
currentTaskStatus = 'done';
|
|
412
571
|
document.getElementById('interrupt-btn').classList.add('hidden');
|
|
413
572
|
document.getElementById('kill-btn').classList.add('hidden');
|
|
573
|
+
document.getElementById('resume-detail-btn').classList.toggle('hidden', !data.claude_session_id);
|
|
414
574
|
if (pollInterval) {
|
|
415
575
|
clearInterval(pollInterval);
|
|
416
576
|
pollInterval = null;
|
|
417
577
|
}
|
|
578
|
+
loadTasks();
|
|
418
579
|
}
|
|
419
580
|
} catch (e) {
|
|
420
581
|
showConnectionError(true);
|
|
@@ -465,17 +626,39 @@
|
|
|
465
626
|
if (!currentTaskId) return;
|
|
466
627
|
if (!confirm('Kill this task?')) return;
|
|
467
628
|
try {
|
|
468
|
-
await fetch(`${API}/tasks/${currentTaskId}`, {method: 'DELETE'});
|
|
629
|
+
const res = await fetch(`${API}/tasks/${currentTaskId}`, {method: 'DELETE'});
|
|
630
|
+
if (!res.ok) {
|
|
631
|
+
const data = await res.json().catch(() => ({}));
|
|
632
|
+
alert(data.detail || 'Failed to kill task');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
469
635
|
showList();
|
|
470
636
|
} catch (e) {
|
|
471
637
|
showConnectionError(true);
|
|
472
638
|
}
|
|
473
639
|
}
|
|
474
640
|
|
|
641
|
+
async function resumeTask(taskId) {
|
|
642
|
+
try {
|
|
643
|
+
const res = await fetch(`${API}/tasks/${taskId}/resume`, {method: 'POST'});
|
|
644
|
+
if (!res.ok) {
|
|
645
|
+
const data = await res.json().catch(() => ({}));
|
|
646
|
+
alert(data.detail || 'Failed to resume task');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const newTask = await res.json();
|
|
650
|
+
await loadTasks();
|
|
651
|
+
showTask(newTask.id);
|
|
652
|
+
} catch (e) {
|
|
653
|
+
showConnectionError(true);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
475
657
|
function showModal() {
|
|
476
658
|
document.getElementById('modal').style.display = 'block';
|
|
477
659
|
document.getElementById('modal-overlay').style.display = 'block';
|
|
478
|
-
|
|
660
|
+
const saved = localStorage.getItem('skipPermissions');
|
|
661
|
+
document.getElementById('skip-perms-input').checked = saved !== null ? saved === '1' : skipPermissionsDefault;
|
|
479
662
|
document.getElementById('prompt-input').focus();
|
|
480
663
|
}
|
|
481
664
|
|
|
@@ -489,6 +672,7 @@
|
|
|
489
672
|
if (!prompt) return;
|
|
490
673
|
const workdir = document.getElementById('workdir-input').value.trim() || null;
|
|
491
674
|
const skipPerms = document.getElementById('skip-perms-input').checked;
|
|
675
|
+
localStorage.setItem('skipPermissions', skipPerms ? '1' : '0');
|
|
492
676
|
const body = {prompt, skip_permissions: skipPerms};
|
|
493
677
|
if (workdir) body.working_dir = workdir;
|
|
494
678
|
try {
|
|
@@ -514,6 +698,110 @@
|
|
|
514
698
|
return d.innerHTML;
|
|
515
699
|
}
|
|
516
700
|
|
|
701
|
+
// --- ANSI-to-HTML converter ---
|
|
702
|
+
const ANSI_FG = [
|
|
703
|
+
'#555','#ff5555','#50fa7b','#f1fa8c','#bd93f9','#ff79c6','#8be9fd','#f8f8f2',
|
|
704
|
+
'#6272a4','#ff6e6e','#69ff94','#ffffa5','#d6acff','#ff92df','#a4ffff','#ffffff',
|
|
705
|
+
];
|
|
706
|
+
|
|
707
|
+
function color256(n) {
|
|
708
|
+
if (n < 16) return ANSI_FG[n] || '#e0e0e0';
|
|
709
|
+
if (n < 232) {
|
|
710
|
+
n -= 16;
|
|
711
|
+
const r = Math.floor(n / 36) * 51;
|
|
712
|
+
const g = Math.floor((n % 36) / 6) * 51;
|
|
713
|
+
const b = (n % 6) * 51;
|
|
714
|
+
return `rgb(${r},${g},${b})`;
|
|
715
|
+
}
|
|
716
|
+
const v = (n - 232) * 10 + 8;
|
|
717
|
+
return `rgb(${v},${v},${v})`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function ansiToHtml(text) {
|
|
721
|
+
text = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
722
|
+
const parts = text.split(/(\x1b\[[\d;]*[a-zA-Z])/);
|
|
723
|
+
let result = '';
|
|
724
|
+
let state = {};
|
|
725
|
+
for (const part of parts) {
|
|
726
|
+
const m = part.match(/^\x1b\[([\d;]*)m$/);
|
|
727
|
+
if (m) {
|
|
728
|
+
const codes = m[1] ? m[1].split(';').map(Number) : [0];
|
|
729
|
+
let i = 0;
|
|
730
|
+
while (i < codes.length) {
|
|
731
|
+
const c = codes[i];
|
|
732
|
+
if (c === 0) state = {};
|
|
733
|
+
else if (c === 1) state.bold = true;
|
|
734
|
+
else if (c === 2) state.dim = true;
|
|
735
|
+
else if (c === 3) state.italic = true;
|
|
736
|
+
else if (c === 4) state.underline = true;
|
|
737
|
+
else if (c === 22) { delete state.bold; delete state.dim; }
|
|
738
|
+
else if (c === 23) delete state.italic;
|
|
739
|
+
else if (c === 24) delete state.underline;
|
|
740
|
+
else if (c >= 30 && c <= 37) state.fg = ANSI_FG[c - 30];
|
|
741
|
+
else if (c >= 90 && c <= 97) state.fg = ANSI_FG[c - 82];
|
|
742
|
+
else if (c === 39) delete state.fg;
|
|
743
|
+
else if (c >= 40 && c <= 47) state.bg = ANSI_FG[c - 40];
|
|
744
|
+
else if (c >= 100 && c <= 107) state.bg = ANSI_FG[c - 92];
|
|
745
|
+
else if (c === 49) delete state.bg;
|
|
746
|
+
else if (c === 38 && codes[i+1] === 5) { state.fg = color256(codes[i+2] || 0); i += 2; }
|
|
747
|
+
else if (c === 38 && codes[i+1] === 2) { state.fg = `rgb(${codes[i+2]||0},${codes[i+3]||0},${codes[i+4]||0})`; i += 4; }
|
|
748
|
+
else if (c === 48 && codes[i+1] === 5) { state.bg = color256(codes[i+2] || 0); i += 2; }
|
|
749
|
+
else if (c === 48 && codes[i+1] === 2) { state.bg = `rgb(${codes[i+2]||0},${codes[i+3]||0},${codes[i+4]||0})`; i += 4; }
|
|
750
|
+
i++;
|
|
751
|
+
}
|
|
752
|
+
} else if (part.match(/^\x1b\[/)) {
|
|
753
|
+
// non-SGR ANSI sequence — skip
|
|
754
|
+
} else if (part) {
|
|
755
|
+
const s = [];
|
|
756
|
+
if (state.bold) s.push('font-weight:bold');
|
|
757
|
+
if (state.dim) s.push('opacity:0.6');
|
|
758
|
+
if (state.italic) s.push('font-style:italic');
|
|
759
|
+
if (state.underline) s.push('text-decoration:underline');
|
|
760
|
+
if (state.fg) s.push('color:' + state.fg);
|
|
761
|
+
if (state.bg) s.push('background:' + state.bg);
|
|
762
|
+
result += s.length ? `<span style="${s.join(';')}">${part}</span>` : part;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return result;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// --- Block parser (user prompts vs claude responses) ---
|
|
769
|
+
const ANSI_STRIP_RE = /\x1b\[[\d;]*[a-zA-Z]/g;
|
|
770
|
+
|
|
771
|
+
function parseBlocks(text) {
|
|
772
|
+
const lines = text.split('\n');
|
|
773
|
+
const blocks = [];
|
|
774
|
+
let cur = null;
|
|
775
|
+
for (const line of lines) {
|
|
776
|
+
const stripped = line.replace(ANSI_STRIP_RE, '').trimStart();
|
|
777
|
+
if (stripped.startsWith('›') || stripped.startsWith('❯')) {
|
|
778
|
+
if (cur) blocks.push(cur);
|
|
779
|
+
cur = { type: 'user', lines: [line] };
|
|
780
|
+
} else if (stripped.startsWith('●') || stripped.startsWith('⏺')) {
|
|
781
|
+
if (cur) blocks.push(cur);
|
|
782
|
+
cur = { type: 'claude', lines: [line] };
|
|
783
|
+
} else if (cur) {
|
|
784
|
+
cur.lines.push(line);
|
|
785
|
+
} else {
|
|
786
|
+
if (!blocks.length || blocks[blocks.length - 1].type !== 'plain') {
|
|
787
|
+
blocks.push({ type: 'plain', lines: [] });
|
|
788
|
+
}
|
|
789
|
+
blocks[blocks.length - 1].lines.push(line);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (cur) blocks.push(cur);
|
|
793
|
+
return blocks;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function renderOutput(text) {
|
|
797
|
+
const blocks = parseBlocks(text);
|
|
798
|
+
return blocks.map(b => {
|
|
799
|
+
const html = ansiToHtml(b.lines.join('\n'));
|
|
800
|
+
const cls = b.type === 'user' ? 'block-user' : b.type === 'claude' ? 'block-claude' : 'block-plain';
|
|
801
|
+
return `<div class="output-block ${cls}">${html}</div>`;
|
|
802
|
+
}).join('');
|
|
803
|
+
}
|
|
804
|
+
|
|
517
805
|
// Event listeners
|
|
518
806
|
document.getElementById('new-task-btn').addEventListener('click', showModal);
|
|
519
807
|
document.getElementById('modal-overlay').addEventListener('click', hideModal);
|
|
@@ -521,6 +809,9 @@
|
|
|
521
809
|
document.getElementById('back-btn').addEventListener('click', showList);
|
|
522
810
|
document.getElementById('interrupt-btn').addEventListener('click', interruptTask);
|
|
523
811
|
document.getElementById('kill-btn').addEventListener('click', killTask);
|
|
812
|
+
document.getElementById('resume-detail-btn').addEventListener('click', () => {
|
|
813
|
+
if (currentTaskId) resumeTask(currentTaskId);
|
|
814
|
+
});
|
|
524
815
|
document.getElementById('send-btn').addEventListener('click', sendMessage);
|
|
525
816
|
document.getElementById('message-input').addEventListener('keydown', e => {
|
|
526
817
|
if (e.key === 'Enter') sendMessage();
|
|
@@ -529,7 +820,15 @@
|
|
|
529
820
|
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') submitTask();
|
|
530
821
|
});
|
|
531
822
|
|
|
823
|
+
document.getElementById('search-input').addEventListener('input', loadTasks);
|
|
824
|
+
document.getElementById('sort-select').addEventListener('change', () => {
|
|
825
|
+
localStorage.setItem('onakoSort', document.getElementById('sort-select').value);
|
|
826
|
+
loadTasks();
|
|
827
|
+
});
|
|
828
|
+
|
|
532
829
|
// Init
|
|
830
|
+
const savedSort = localStorage.getItem('onakoSort');
|
|
831
|
+
if (savedSort) document.getElementById('sort-select').value = savedSort;
|
|
533
832
|
loadConfig();
|
|
534
833
|
loadTasks();
|
|
535
834
|
setInterval(() => { if (!currentTaskId || isDesktop()) loadTasks(); }, 10000);
|
onako/tmux_orchestrator.py
CHANGED
|
@@ -3,6 +3,7 @@ import secrets
|
|
|
3
3
|
import shlex
|
|
4
4
|
import sqlite3
|
|
5
5
|
import subprocess
|
|
6
|
+
import uuid
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -16,6 +17,7 @@ class TmuxOrchestrator:
|
|
|
16
17
|
self.db_path = db_path or DB_PATH
|
|
17
18
|
self.tasks: dict[str, dict] = {}
|
|
18
19
|
self._window_ids: dict[str, str] = {} # window_name -> @id
|
|
20
|
+
self._last_content_length: dict[str, int] = {} # window_name -> pane content length
|
|
19
21
|
self._init_db()
|
|
20
22
|
self._load_tasks()
|
|
21
23
|
self._ensure_session()
|
|
@@ -46,20 +48,26 @@ class TmuxOrchestrator:
|
|
|
46
48
|
prompt TEXT,
|
|
47
49
|
status TEXT,
|
|
48
50
|
started_at TEXT,
|
|
49
|
-
origin TEXT DEFAULT 'managed'
|
|
51
|
+
origin TEXT DEFAULT 'managed',
|
|
52
|
+
last_activity TEXT,
|
|
53
|
+
claude_session_id TEXT
|
|
50
54
|
)
|
|
51
55
|
""")
|
|
52
|
-
# Migrate existing DBs that lack
|
|
56
|
+
# Migrate existing DBs that lack newer columns
|
|
53
57
|
cursor = conn.execute("PRAGMA table_info(tasks)")
|
|
54
58
|
columns = [row[1] for row in cursor.fetchall()]
|
|
55
59
|
if "origin" not in columns:
|
|
56
60
|
conn.execute("ALTER TABLE tasks ADD COLUMN origin TEXT DEFAULT 'managed'")
|
|
61
|
+
if "last_activity" not in columns:
|
|
62
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN last_activity TEXT")
|
|
63
|
+
if "claude_session_id" not in columns:
|
|
64
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN claude_session_id TEXT")
|
|
57
65
|
conn.commit()
|
|
58
66
|
conn.close()
|
|
59
67
|
|
|
60
68
|
def _load_tasks(self):
|
|
61
69
|
conn = sqlite3.connect(self.db_path)
|
|
62
|
-
rows = conn.execute("SELECT id, prompt, status, started_at, origin FROM tasks").fetchall()
|
|
70
|
+
rows = conn.execute("SELECT id, prompt, status, started_at, origin, last_activity, claude_session_id FROM tasks").fetchall()
|
|
63
71
|
conn.close()
|
|
64
72
|
for row in rows:
|
|
65
73
|
self.tasks[row[0]] = {
|
|
@@ -68,13 +76,15 @@ class TmuxOrchestrator:
|
|
|
68
76
|
"status": row[2],
|
|
69
77
|
"started_at": row[3],
|
|
70
78
|
"origin": row[4] or "managed",
|
|
79
|
+
"last_activity": row[5],
|
|
80
|
+
"claude_session_id": row[6],
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
def _save_task(self, task: dict):
|
|
74
84
|
conn = sqlite3.connect(self.db_path)
|
|
75
85
|
conn.execute(
|
|
76
|
-
"INSERT OR REPLACE INTO tasks (id, prompt, status, started_at, origin) VALUES (?, ?, ?, ?, ?)",
|
|
77
|
-
(task["id"], task["prompt"], task["status"], task["started_at"], task.get("origin", "managed")),
|
|
86
|
+
"INSERT OR REPLACE INTO tasks (id, prompt, status, started_at, origin, last_activity, claude_session_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
87
|
+
(task["id"], task["prompt"], task["status"], task["started_at"], task.get("origin", "managed"), task.get("last_activity"), task.get("claude_session_id")),
|
|
78
88
|
)
|
|
79
89
|
conn.commit()
|
|
80
90
|
conn.close()
|
|
@@ -89,7 +99,7 @@ class TmuxOrchestrator:
|
|
|
89
99
|
return window_id
|
|
90
100
|
return f"{self.session_name}:{task_id}"
|
|
91
101
|
|
|
92
|
-
def create_task(self, command: str, working_dir: str | None = None, prompt: str | None = None) -> dict:
|
|
102
|
+
def create_task(self, command: str, working_dir: str | None = None, prompt: str | None = None, claude_session_id: str | None = None) -> dict:
|
|
93
103
|
self._ensure_session()
|
|
94
104
|
task_id = f"task-{secrets.token_hex(4)}"
|
|
95
105
|
self._run_tmux(
|
|
@@ -118,34 +128,56 @@ class TmuxOrchestrator:
|
|
|
118
128
|
"send-keys", "-t", self._task_target(task_id),
|
|
119
129
|
"Enter",
|
|
120
130
|
)
|
|
131
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
121
132
|
task = {
|
|
122
133
|
"id": task_id,
|
|
123
134
|
"prompt": prompt or command,
|
|
124
135
|
"status": "running",
|
|
125
|
-
"started_at":
|
|
136
|
+
"started_at": now,
|
|
126
137
|
"origin": "managed",
|
|
138
|
+
"last_activity": now,
|
|
139
|
+
"claude_session_id": claude_session_id,
|
|
127
140
|
}
|
|
128
141
|
self.tasks[task_id] = task
|
|
129
142
|
self._save_task(task)
|
|
130
143
|
return task
|
|
131
144
|
|
|
145
|
+
def resume_task(self, task_id: str, skip_permissions: bool = False) -> dict:
|
|
146
|
+
"""Resume a done task by its claude_session_id."""
|
|
147
|
+
task = self.tasks.get(task_id)
|
|
148
|
+
if not task:
|
|
149
|
+
raise ValueError("Task not found")
|
|
150
|
+
if task["status"] == "running":
|
|
151
|
+
raise ValueError("Task is still running")
|
|
152
|
+
session_id = task.get("claude_session_id")
|
|
153
|
+
if not session_id:
|
|
154
|
+
raise ValueError("Task has no claude_session_id (cannot resume)")
|
|
155
|
+
cmd = "claude"
|
|
156
|
+
if skip_permissions:
|
|
157
|
+
cmd += " --dangerously-skip-permissions"
|
|
158
|
+
cmd += f" --resume {session_id}"
|
|
159
|
+
task["status"] = "resumed"
|
|
160
|
+
self._save_task(task)
|
|
161
|
+
return self.create_task(cmd, prompt=f"(resumed) {task['prompt']}", claude_session_id=session_id)
|
|
162
|
+
|
|
132
163
|
def list_tasks(self) -> list[dict]:
|
|
133
164
|
self.rediscover_tasks()
|
|
134
165
|
self._sync_task_status()
|
|
135
166
|
return list(self.tasks.values())
|
|
136
167
|
|
|
168
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
169
|
+
|
|
137
170
|
def get_output(self, task_id: str) -> str:
|
|
138
171
|
raw = self.get_raw_output(task_id)
|
|
139
|
-
|
|
140
|
-
return self._strip_claude_chrome(cleaned)
|
|
172
|
+
return self._strip_claude_chrome(raw)
|
|
141
173
|
|
|
142
|
-
@
|
|
143
|
-
def _strip_claude_chrome(text: str) -> str:
|
|
174
|
+
@classmethod
|
|
175
|
+
def _strip_claude_chrome(cls, text: str) -> str:
|
|
144
176
|
lines = text.split("\n")
|
|
145
177
|
# Strip from the bottom: Claude Code's TUI draws box-drawing chars,
|
|
146
178
|
# the › prompt, and status lines like "accept edits on..."
|
|
147
179
|
while lines:
|
|
148
|
-
line = lines[-1].strip()
|
|
180
|
+
line = cls._ANSI_RE.sub("", lines[-1]).strip()
|
|
149
181
|
if (
|
|
150
182
|
not line
|
|
151
183
|
or all(c in "─━╭╮╰╯│┃┌┐└┘├┤┬┴┼╋═║ ›❯▸▶" for c in line)
|
|
@@ -163,7 +195,7 @@ class TmuxOrchestrator:
|
|
|
163
195
|
def get_raw_output(self, task_id: str) -> str:
|
|
164
196
|
result = self._run_tmux(
|
|
165
197
|
"capture-pane", "-t", self._task_target(task_id),
|
|
166
|
-
"-p", "-S", "-",
|
|
198
|
+
"-p", "-S", "-", "-e",
|
|
167
199
|
)
|
|
168
200
|
return result.stdout
|
|
169
201
|
|
|
@@ -180,6 +212,14 @@ class TmuxOrchestrator:
|
|
|
180
212
|
def send_interrupt(self, task_id: str):
|
|
181
213
|
self._run_tmux("send-keys", "-t", self._task_target(task_id), "Escape")
|
|
182
214
|
|
|
215
|
+
def window_count(self) -> int:
|
|
216
|
+
result = self._run_tmux(
|
|
217
|
+
"list-windows", "-t", self._session_target, "-F", "#{window_name}",
|
|
218
|
+
)
|
|
219
|
+
if result.stdout.strip():
|
|
220
|
+
return len(result.stdout.strip().split("\n"))
|
|
221
|
+
return 0
|
|
222
|
+
|
|
183
223
|
def kill_task(self, task_id: str):
|
|
184
224
|
self._run_tmux("kill-window", "-t", self._task_target(task_id))
|
|
185
225
|
if task_id in self.tasks:
|
|
@@ -201,6 +241,16 @@ class TmuxOrchestrator:
|
|
|
201
241
|
if task["status"] == "running" and task_id not in active_windows:
|
|
202
242
|
task["status"] = "done"
|
|
203
243
|
self._save_task(task)
|
|
244
|
+
elif task["status"] == "running" and task_id in active_windows:
|
|
245
|
+
pane = self._run_tmux(
|
|
246
|
+
"capture-pane", "-t", self._task_target(task_id), "-p",
|
|
247
|
+
)
|
|
248
|
+
content_len = len(pane.stdout)
|
|
249
|
+
prev_len = self._last_content_length.get(task_id)
|
|
250
|
+
self._last_content_length[task_id] = content_len
|
|
251
|
+
if prev_len is not None and content_len != prev_len:
|
|
252
|
+
task["last_activity"] = datetime.now(timezone.utc).isoformat()
|
|
253
|
+
self._save_task(task)
|
|
204
254
|
|
|
205
255
|
def rediscover_tasks(self):
|
|
206
256
|
"""Rediscover tasks from existing tmux windows on server restart."""
|
|
@@ -216,16 +266,28 @@ class TmuxOrchestrator:
|
|
|
216
266
|
if window_id:
|
|
217
267
|
self._window_ids[window_name] = window_id
|
|
218
268
|
if window_name not in self.tasks:
|
|
219
|
-
is_managed = window_name.startswith("task-")
|
|
269
|
+
is_managed = window_name.startswith("task-") or window_name == "onako-main"
|
|
270
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
220
271
|
task = {
|
|
221
272
|
"id": window_name,
|
|
222
273
|
"prompt": "(rediscovered)" if is_managed else window_name,
|
|
223
274
|
"status": "running",
|
|
224
275
|
"started_at": None,
|
|
225
276
|
"origin": "managed" if is_managed else "external",
|
|
277
|
+
"last_activity": now,
|
|
226
278
|
}
|
|
227
279
|
self.tasks[window_name] = task
|
|
228
280
|
self._save_task(task)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
self.
|
|
281
|
+
else:
|
|
282
|
+
changed = False
|
|
283
|
+
if self.tasks[window_name]["status"] == "done":
|
|
284
|
+
self.tasks[window_name]["status"] = "running"
|
|
285
|
+
changed = True
|
|
286
|
+
if window_name == "onako-main" and self.tasks[window_name]["origin"] == "external":
|
|
287
|
+
self.tasks[window_name]["origin"] = "managed"
|
|
288
|
+
changed = True
|
|
289
|
+
if not self.tasks[window_name].get("last_activity"):
|
|
290
|
+
self.tasks[window_name]["last_activity"] = datetime.now(timezone.utc).isoformat()
|
|
291
|
+
changed = True
|
|
292
|
+
if changed:
|
|
293
|
+
self._save_task(self.tasks[window_name])
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
onako/__init__.py,sha256=6G_giX6Ucuweo7w5OiftoXmbNLoqiU_soXJoU8aiLmY,22
|
|
2
|
+
onako/cli.py,sha256=sQgN07VmqF9yklWQrbEdc1PE7PsI94oAJIELiaJdVfI,10233
|
|
3
|
+
onako/server.py,sha256=UL51fROpjQirh8Rwi9WFRPv9xmITpDKrdD8_xsMNc-8,3420
|
|
4
|
+
onako/tmux_orchestrator.py,sha256=yIgnu8T9CmdW9B91JjWJxA-nX51TmMpaPHRgflKnvs0,11703
|
|
5
|
+
onako/static/index.html,sha256=_966Z2fdyJjF0N_TJdvex1Hyo14HBhmhQI_CVTXUKMo,32637
|
|
6
|
+
onako-0.4.4.dist-info/METADATA,sha256=Uu8bxV-TfTpj7PTdNa5G_psnI4kvIfmwz5XMhVxO30I,2175
|
|
7
|
+
onako-0.4.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
+
onako-0.4.4.dist-info/entry_points.txt,sha256=51KRJzuVpr69iT_k4JO0Lj3DQv_HbgtGjTBTev13JAQ,41
|
|
9
|
+
onako-0.4.4.dist-info/top_level.txt,sha256=EZsc5qq2paM9GTbaFE9Xar4B5wFdfIqK9l_bDQVcmZ4,6
|
|
10
|
+
onako-0.4.4.dist-info/RECORD,,
|
onako-0.4.3.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
onako/__init__.py,sha256=Nyg0pmk5ea9-SLCAFEIF96ByFx4-TJFtrqYPN-Zn6g4,22
|
|
2
|
-
onako/cli.py,sha256=wcrpaTA1GZNlLghBmrgEnuYzQIDyBte0NqpLhY0mX6w,9541
|
|
3
|
-
onako/server.py,sha256=NfP2CaecxAzuq0tSIaatfYX3d98EuLfhIwK00oHNjro,2883
|
|
4
|
-
onako/tmux_orchestrator.py,sha256=5RPtZ3B0dBeKMEJjYLI54AIJaOVncSp6los2CJ65szA,8572
|
|
5
|
-
onako/static/index.html,sha256=6fXfObv2Gtj3A7yBWYtiee-fdfDXtTNl092i7PiRebY,19383
|
|
6
|
-
onako-0.4.3.dist-info/METADATA,sha256=mfgLx5hEXot1M83Xe93J8LY0srayEwQ4z41raSTfhMY,2175
|
|
7
|
-
onako-0.4.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
-
onako-0.4.3.dist-info/entry_points.txt,sha256=51KRJzuVpr69iT_k4JO0Lj3DQv_HbgtGjTBTev13JAQ,41
|
|
9
|
-
onako-0.4.3.dist-info/top_level.txt,sha256=EZsc5qq2paM9GTbaFE9Xar4B5wFdfIqK9l_bDQVcmZ4,6
|
|
10
|
-
onako-0.4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|