onako 0.4.1__py3-none-any.whl → 0.4.3__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.4.1"
1
+ __version__ = "0.4.3"
onako/cli.py CHANGED
@@ -125,6 +125,55 @@ def stop():
125
125
  click.echo("Onako service is not running.")
126
126
 
127
127
 
128
+ @main.command()
129
+ @click.option("--session", default="onako", help="tmux session name.")
130
+ def purge(session):
131
+ """Kill all tmux windows from the onako session and clean up."""
132
+ # Auto-detect current tmux session if inside one
133
+ if os.environ.get("TMUX"):
134
+ try:
135
+ result = subprocess.run(
136
+ ["tmux", "display-message", "-p", "#S"],
137
+ capture_output=True, text=True,
138
+ )
139
+ if result.returncode == 0 and result.stdout.strip():
140
+ session = result.stdout.strip()
141
+ except FileNotFoundError:
142
+ pass
143
+
144
+ # Also stop the server if it's running
145
+ if PID_FILE.exists():
146
+ try:
147
+ pid = int(PID_FILE.read_text().strip())
148
+ os.kill(pid, 15)
149
+ click.echo(f"Stopped onako server (pid {pid}).")
150
+ except (ValueError, ProcessLookupError):
151
+ pass
152
+ PID_FILE.unlink(missing_ok=True)
153
+
154
+ # Kill the tmux session
155
+ result = subprocess.run(
156
+ ["tmux", "kill-session", "-t", f"{session}:"],
157
+ capture_output=True, text=True,
158
+ )
159
+ if result.returncode == 0:
160
+ click.echo(f"Killed tmux session: {session}")
161
+ else:
162
+ click.echo(f"No tmux session '{session}' found.")
163
+
164
+ # Clean up the database
165
+ db_path = ONAKO_DIR / "onako.db"
166
+ if db_path.exists():
167
+ import sqlite3
168
+ conn = sqlite3.connect(db_path)
169
+ conn.execute("UPDATE tasks SET status = 'done' WHERE status = 'running'")
170
+ conn.commit()
171
+ conn.close()
172
+ click.echo("Marked all running tasks as done.")
173
+
174
+ click.echo("Purge complete.")
175
+
176
+
128
177
  @main.command()
129
178
  def status():
130
179
  """Check if Onako is running."""
onako/static/index.html CHANGED
@@ -40,6 +40,7 @@
40
40
  cursor: pointer;
41
41
  }
42
42
  .task-item:hover { background: #222; }
43
+ .task-item.selected { background: #2a2a2a; }
43
44
  .task-item .task-id { font-size: 12px; color: #888; }
44
45
  .task-item .task-prompt {
45
46
  font-size: 14px;
@@ -83,6 +84,14 @@
83
84
  #detail-view { display: none; flex-direction: column; height: 100dvh; }
84
85
  #detail-view.active { display: flex; }
85
86
  #list-view.hidden { display: none; }
87
+ #detail-empty {
88
+ display: none;
89
+ flex: 1;
90
+ align-items: center;
91
+ justify-content: center;
92
+ color: #555;
93
+ font-size: 14px;
94
+ }
86
95
  #detail-header {
87
96
  padding: 12px 16px;
88
97
  padding-top: max(12px, env(safe-area-inset-top));
@@ -212,12 +221,51 @@
212
221
  font-size: 16px;
213
222
  margin-top: 12px;
214
223
  }
224
+
225
+ /* Desktop sidebar layout */
226
+ @media (min-width: 768px) {
227
+ #app-container {
228
+ display: flex;
229
+ height: 100dvh;
230
+ }
231
+ #list-view,
232
+ #list-view.hidden {
233
+ width: 280px;
234
+ flex-shrink: 0;
235
+ border-right: 1px solid #333;
236
+ display: flex;
237
+ flex-direction: column;
238
+ overflow: hidden;
239
+ }
240
+ .task-list {
241
+ flex: 1;
242
+ overflow-y: auto;
243
+ }
244
+ #detail-view {
245
+ flex: 1;
246
+ display: flex;
247
+ height: auto;
248
+ }
249
+ #detail-view:not(.active) #detail-header,
250
+ #detail-view:not(.active) #output,
251
+ #detail-view:not(.active) #message-bar {
252
+ display: none;
253
+ }
254
+ #detail-view:not(.active) #detail-empty {
255
+ display: flex;
256
+ }
257
+ #detail-view.active #detail-empty {
258
+ display: none;
259
+ }
260
+ #back-btn { display: none; }
261
+ }
215
262
  </style>
216
263
  </head>
217
264
  <body>
218
265
  <div id="connection-banner">Connection lost. Retrying...</div>
219
266
 
220
- <!-- List View -->
267
+ <div id="app-container">
268
+ <!-- List View / Sidebar -->
221
269
  <div id="list-view">
222
270
  <header>
223
271
  <h1>Onako</h1>
@@ -231,14 +279,18 @@
231
279
  <div id="detail-header">
232
280
  <button id="back-btn">&larr; Back</button>
233
281
  <span id="detail-task-id"></span>
234
- <button id="interrupt-btn">Interrupt</button>
235
- <button id="kill-btn">Kill</button>
282
+ <div style="display:flex;gap:8px">
283
+ <button id="interrupt-btn">Interrupt</button>
284
+ <button id="kill-btn">Kill</button>
285
+ </div>
236
286
  </div>
237
287
  <div id="output"></div>
238
288
  <div id="message-bar">
239
289
  <input id="message-input" type="text" placeholder="Send a message...">
240
290
  <button id="send-btn">Send</button>
241
291
  </div>
292
+ <div id="detail-empty">Select a task to view</div>
293
+ </div>
242
294
  </div>
243
295
 
244
296
  <!-- New Task Modal -->
@@ -260,6 +312,10 @@
260
312
  let pollInterval = null;
261
313
  let skipPermissionsDefault = false;
262
314
 
315
+ function isDesktop() {
316
+ return window.innerWidth >= 768;
317
+ }
318
+
263
319
  function timeAgo(dateStr) {
264
320
  if (!dateStr) return '';
265
321
  const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
@@ -294,7 +350,7 @@
294
350
  list.innerHTML = '<div class="empty-state">No tasks running</div>';
295
351
  } else {
296
352
  list.innerHTML = tasks.map(t => `
297
- <div class="task-item" onclick="showTask('${t.id}')">
353
+ <div class="task-item${currentTaskId === t.id ? ' selected' : ''}" onclick="showTask('${t.id}')">
298
354
  <div class="task-id">${t.id}${t.origin === 'external' ? '<span class="origin-badge">external</span>' : ''}</div>
299
355
  <div class="task-prompt">${escapeHtml(t.prompt)}</div>
300
356
  <div class="task-meta">
@@ -313,9 +369,19 @@
313
369
  async function showTask(id) {
314
370
  currentTaskId = id;
315
371
  currentTaskStatus = null;
316
- document.getElementById('list-view').classList.add('hidden');
317
- document.getElementById('detail-view').classList.add('active');
318
372
  document.getElementById('detail-task-id').textContent = id;
373
+ if (isDesktop()) {
374
+ document.getElementById('detail-view').classList.add('active');
375
+ document.querySelectorAll('.task-item').forEach(el => el.classList.remove('selected'));
376
+ document.querySelectorAll('.task-item').forEach(el => {
377
+ if (el.onclick && el.getAttribute('onclick')?.includes(id)) {
378
+ el.classList.add('selected');
379
+ }
380
+ });
381
+ } else {
382
+ document.getElementById('list-view').classList.add('hidden');
383
+ document.getElementById('detail-view').classList.add('active');
384
+ }
319
385
  if (id === 'onako-main') {
320
386
  document.getElementById('interrupt-btn').classList.add('hidden');
321
387
  document.getElementById('kill-btn').classList.add('hidden');
@@ -323,6 +389,7 @@
323
389
  document.getElementById('interrupt-btn').classList.remove('hidden');
324
390
  document.getElementById('kill-btn').classList.remove('hidden');
325
391
  }
392
+ if (pollInterval) clearInterval(pollInterval);
326
393
  await refreshOutput();
327
394
  pollInterval = setInterval(refreshOutput, 3000);
328
395
  }
@@ -359,8 +426,13 @@
359
426
  currentTaskStatus = null;
360
427
  if (pollInterval) clearInterval(pollInterval);
361
428
  pollInterval = null;
362
- document.getElementById('detail-view').classList.remove('active');
363
- document.getElementById('list-view').classList.remove('hidden');
429
+ if (isDesktop()) {
430
+ document.getElementById('detail-view').classList.remove('active');
431
+ document.querySelectorAll('.task-item').forEach(el => el.classList.remove('selected'));
432
+ } else {
433
+ document.getElementById('detail-view').classList.remove('active');
434
+ document.getElementById('list-view').classList.remove('hidden');
435
+ }
364
436
  loadTasks();
365
437
  }
366
438
 
@@ -429,6 +501,7 @@
429
501
  document.getElementById('prompt-input').value = '';
430
502
  document.getElementById('workdir-input').value = '';
431
503
  hideModal();
504
+ await loadTasks();
432
505
  showTask(task.id);
433
506
  } catch (e) {
434
507
  showConnectionError(true);
@@ -459,7 +532,7 @@
459
532
  // Init
460
533
  loadConfig();
461
534
  loadTasks();
462
- setInterval(() => { if (!currentTaskId) loadTasks(); }, 10000);
535
+ setInterval(() => { if (!currentTaskId || isDesktop()) loadTasks(); }, 10000);
463
536
  </script>
464
537
  </body>
465
538
  </html>
@@ -21,9 +21,14 @@ class TmuxOrchestrator:
21
21
  self._ensure_session()
22
22
  self.rediscover_tasks()
23
23
 
24
+ @property
25
+ def _session_target(self) -> str:
26
+ """Session name with colon suffix for unambiguous tmux targeting."""
27
+ return f"{self.session_name}:"
28
+
24
29
  def _ensure_session(self):
25
30
  result = subprocess.run(
26
- ["tmux", "has-session", "-t", self.session_name],
31
+ ["tmux", "has-session", "-t", self._session_target],
27
32
  capture_output=True,
28
33
  )
29
34
  if result.returncode != 0:
@@ -88,11 +93,11 @@ class TmuxOrchestrator:
88
93
  self._ensure_session()
89
94
  task_id = f"task-{secrets.token_hex(4)}"
90
95
  self._run_tmux(
91
- "new-window", "-t", self.session_name, "-n", task_id,
96
+ "new-window", "-t", self._session_target, "-n", task_id,
92
97
  )
93
98
  # Look up the window ID for safe targeting
94
99
  result = self._run_tmux(
95
- "list-windows", "-t", self.session_name, "-F", "#{window_name}|#{window_id}",
100
+ "list-windows", "-t", self._session_target, "-F", "#{window_name}|#{window_id}",
96
101
  )
97
102
  if result.stdout.strip():
98
103
  for line in result.stdout.strip().split("\n"):
@@ -107,7 +112,11 @@ class TmuxOrchestrator:
107
112
  )
108
113
  self._run_tmux(
109
114
  "send-keys", "-t", self._task_target(task_id),
110
- command, "Enter",
115
+ "-l", command,
116
+ )
117
+ self._run_tmux(
118
+ "send-keys", "-t", self._task_target(task_id),
119
+ "Enter",
111
120
  )
112
121
  task = {
113
122
  "id": task_id,
@@ -179,7 +188,7 @@ class TmuxOrchestrator:
179
188
 
180
189
  def _sync_task_status(self):
181
190
  result = self._run_tmux(
182
- "list-windows", "-t", self.session_name, "-F", "#{window_name}|#{window_id}",
191
+ "list-windows", "-t", self._session_target, "-F", "#{window_name}|#{window_id}",
183
192
  )
184
193
  active_windows = set()
185
194
  if result.stdout.strip():
@@ -196,7 +205,7 @@ class TmuxOrchestrator:
196
205
  def rediscover_tasks(self):
197
206
  """Rediscover tasks from existing tmux windows on server restart."""
198
207
  result = self._run_tmux(
199
- "list-windows", "-t", self.session_name, "-F", "#{window_name}|#{window_id}",
208
+ "list-windows", "-t", self._session_target, "-F", "#{window_name}|#{window_id}",
200
209
  )
201
210
  if not result.stdout.strip():
202
211
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onako
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Dispatch and monitor Claude Code tasks from your phone
5
5
  Author: Amir
6
6
  License-Expression: MIT
@@ -0,0 +1,10 @@
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,,
@@ -1,10 +0,0 @@
1
- onako/__init__.py,sha256=pMtTmSUht-XtbR_7Doz6bsQqopJJd8rZ8I8zy2HwwoA,22
2
- onako/cli.py,sha256=_-dWcf7fVD3QmN6wIzbFLydEVX2pzGs2cKBwP6SZWtY,7932
3
- onako/server.py,sha256=NfP2CaecxAzuq0tSIaatfYX3d98EuLfhIwK00oHNjro,2883
4
- onako/tmux_orchestrator.py,sha256=URAOdYo88SzW1ef0o9_hKxJ0SeTtuqZlex1kgTMv42w,8277
5
- onako/static/index.html,sha256=ioombLZg8uhVHL64saB50ikYBUYNDgw5yClA-QoNxEo,16802
6
- onako-0.4.1.dist-info/METADATA,sha256=WBaCh-ead_JtD_avuSMhUeJD0dnBl7kLor7UIXPFrB4,2175
7
- onako-0.4.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
- onako-0.4.1.dist-info/entry_points.txt,sha256=51KRJzuVpr69iT_k4JO0Lj3DQv_HbgtGjTBTev13JAQ,41
9
- onako-0.4.1.dist-info/top_level.txt,sha256=EZsc5qq2paM9GTbaFE9Xar4B5wFdfIqK9l_bDQVcmZ4,6
10
- onako-0.4.1.dist-info/RECORD,,
File without changes