aline-ai 0.5.12__py3-none-any.whl → 0.6.0__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.
- {aline_ai-0.5.12.dist-info → aline_ai-0.6.0.dist-info}/METADATA +1 -1
- {aline_ai-0.5.12.dist-info → aline_ai-0.6.0.dist-info}/RECORD +25 -23
- realign/__init__.py +1 -1
- realign/auth.py +539 -0
- realign/cli.py +272 -1
- realign/commands/auth.py +343 -0
- realign/commands/export_shares.py +44 -21
- realign/commands/import_shares.py +16 -10
- realign/commands/init.py +10 -33
- realign/commands/watcher.py +19 -16
- realign/commands/worker.py +8 -0
- realign/config.py +12 -29
- realign/dashboard/widgets/config_panel.py +177 -1
- realign/dashboard/widgets/events_table.py +44 -5
- realign/dashboard/widgets/sessions_table.py +76 -16
- realign/db/base.py +28 -11
- realign/db/schema.py +102 -15
- realign/db/sqlite_db.py +108 -58
- realign/watcher_core.py +1 -9
- realign/watcher_daemon.py +11 -0
- realign/worker_daemon.py +11 -0
- {aline_ai-0.5.12.dist-info → aline_ai-0.6.0.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.12.dist-info → aline_ai-0.6.0.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.12.dist-info → aline_ai-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.12.dist-info → aline_ai-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
"""Config Panel Widget for viewing and editing configuration."""
|
|
2
2
|
|
|
3
|
+
import threading
|
|
4
|
+
import webbrowser
|
|
3
5
|
from dataclasses import fields
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Optional
|
|
6
8
|
|
|
7
9
|
from textual.app import ComposeResult
|
|
8
|
-
from textual.containers import Horizontal
|
|
10
|
+
from textual.containers import Horizontal, Vertical
|
|
9
11
|
from textual.widgets import Button, DataTable, Input, Static, Switch
|
|
10
12
|
|
|
11
13
|
from ..tmux_manager import _run_outer_tmux, OUTER_SESSION
|
|
14
|
+
from ...auth import (
|
|
15
|
+
load_credentials,
|
|
16
|
+
save_credentials,
|
|
17
|
+
clear_credentials,
|
|
18
|
+
open_login_page,
|
|
19
|
+
is_logged_in,
|
|
20
|
+
get_current_user,
|
|
21
|
+
find_free_port,
|
|
22
|
+
start_callback_server,
|
|
23
|
+
validate_cli_token,
|
|
24
|
+
)
|
|
25
|
+
from ...config import ReAlignConfig
|
|
26
|
+
from ...logging_config import setup_logger
|
|
27
|
+
|
|
28
|
+
logger = setup_logger("realign.dashboard.widgets.config_panel", "dashboard.log")
|
|
12
29
|
|
|
13
30
|
|
|
14
31
|
class ConfigPanel(Static):
|
|
@@ -74,6 +91,21 @@ class ConfigPanel(Static):
|
|
|
74
91
|
ConfigPanel .tmux-settings Switch {
|
|
75
92
|
width: auto;
|
|
76
93
|
}
|
|
94
|
+
|
|
95
|
+
ConfigPanel .account-section {
|
|
96
|
+
height: auto;
|
|
97
|
+
margin-top: 1;
|
|
98
|
+
padding: 1;
|
|
99
|
+
border: solid $success;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ConfigPanel .account-section .account-status {
|
|
103
|
+
margin-bottom: 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ConfigPanel .account-section Button {
|
|
107
|
+
margin-right: 1;
|
|
108
|
+
}
|
|
77
109
|
"""
|
|
78
110
|
|
|
79
111
|
def __init__(self) -> None:
|
|
@@ -83,6 +115,8 @@ class ConfigPanel(Static):
|
|
|
83
115
|
self._selected_key: Optional[str] = None
|
|
84
116
|
self._border_resize_enabled: bool = True # Track tmux border resize state
|
|
85
117
|
self._syncing_switch: bool = False # Flag to prevent recursive switch updates
|
|
118
|
+
self._login_in_progress: bool = False # Track login state
|
|
119
|
+
self._refresh_timer = None # Timer for auto-refresh
|
|
86
120
|
|
|
87
121
|
def compose(self) -> ComposeResult:
|
|
88
122
|
"""Compose the config panel layout."""
|
|
@@ -96,6 +130,14 @@ class ConfigPanel(Static):
|
|
|
96
130
|
yield Button("Save", id="save-btn", variant="primary")
|
|
97
131
|
yield Button("Reload", id="reload-btn", variant="default")
|
|
98
132
|
|
|
133
|
+
# Account section
|
|
134
|
+
with Static(classes="account-section"):
|
|
135
|
+
yield Static("[bold]Account[/bold]", classes="section-title")
|
|
136
|
+
yield Static(id="account-status", classes="account-status")
|
|
137
|
+
with Horizontal(classes="button-row"):
|
|
138
|
+
yield Button("Login", id="login-btn", variant="primary")
|
|
139
|
+
yield Button("Logout", id="logout-btn", variant="warning")
|
|
140
|
+
|
|
99
141
|
# Tmux settings section
|
|
100
142
|
with Static(classes="tmux-settings"):
|
|
101
143
|
yield Static("[bold]Tmux Settings[/bold]", classes="section-title")
|
|
@@ -112,9 +154,15 @@ class ConfigPanel(Static):
|
|
|
112
154
|
# Load initial data
|
|
113
155
|
self.refresh_data()
|
|
114
156
|
|
|
157
|
+
# Update account status display
|
|
158
|
+
self._update_account_status()
|
|
159
|
+
|
|
115
160
|
# Query and set the actual tmux border resize state
|
|
116
161
|
self._sync_border_resize_switch()
|
|
117
162
|
|
|
163
|
+
# Start timer to periodically refresh account status (every 5 seconds)
|
|
164
|
+
self._refresh_timer = self.set_interval(5.0, self._update_account_status)
|
|
165
|
+
|
|
118
166
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
119
167
|
"""Handle row selection in the config table."""
|
|
120
168
|
table = self.query_one("#config-table", DataTable)
|
|
@@ -147,7 +195,12 @@ class ConfigPanel(Static):
|
|
|
147
195
|
self._save_config()
|
|
148
196
|
elif event.button.id == "reload-btn":
|
|
149
197
|
self.refresh_data()
|
|
198
|
+
self._update_account_status()
|
|
150
199
|
self.app.notify("Configuration reloaded", title="Config")
|
|
200
|
+
elif event.button.id == "login-btn":
|
|
201
|
+
self._handle_login()
|
|
202
|
+
elif event.button.id == "logout-btn":
|
|
203
|
+
self._handle_logout()
|
|
151
204
|
|
|
152
205
|
def on_switch_changed(self, event: Switch.Changed) -> None:
|
|
153
206
|
"""Handle switch toggle events."""
|
|
@@ -156,6 +209,129 @@ class ConfigPanel(Static):
|
|
|
156
209
|
if event.switch.id == "border-resize-switch":
|
|
157
210
|
self._toggle_border_resize(event.value)
|
|
158
211
|
|
|
212
|
+
def _update_account_status(self) -> None:
|
|
213
|
+
"""Update the account status display."""
|
|
214
|
+
try:
|
|
215
|
+
status_widget = self.query_one("#account-status", Static)
|
|
216
|
+
login_btn = self.query_one("#login-btn", Button)
|
|
217
|
+
logout_btn = self.query_one("#logout-btn", Button)
|
|
218
|
+
except Exception:
|
|
219
|
+
# Widget not ready yet
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# Don't update if login is in progress
|
|
223
|
+
if self._login_in_progress:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
credentials = get_current_user()
|
|
227
|
+
if credentials:
|
|
228
|
+
status_widget.update(
|
|
229
|
+
f"[green]Logged in as:[/green] [bold]{credentials.email}[/bold]"
|
|
230
|
+
)
|
|
231
|
+
login_btn.disabled = True
|
|
232
|
+
logout_btn.disabled = False
|
|
233
|
+
else:
|
|
234
|
+
status_widget.update("[yellow]Not logged in[/yellow]")
|
|
235
|
+
login_btn.disabled = False
|
|
236
|
+
logout_btn.disabled = True
|
|
237
|
+
|
|
238
|
+
def _handle_login(self) -> None:
|
|
239
|
+
"""Handle login button click - start login flow in background."""
|
|
240
|
+
if self._login_in_progress:
|
|
241
|
+
self.app.notify("Login already in progress...", title="Login")
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
self._login_in_progress = True
|
|
245
|
+
|
|
246
|
+
# Update UI to show login in progress
|
|
247
|
+
login_btn = self.query_one("#login-btn", Button)
|
|
248
|
+
login_btn.disabled = True
|
|
249
|
+
status_widget = self.query_one("#account-status", Static)
|
|
250
|
+
status_widget.update("[cyan]Opening browser for login...[/cyan]")
|
|
251
|
+
|
|
252
|
+
# Start login flow in background thread
|
|
253
|
+
def do_login():
|
|
254
|
+
try:
|
|
255
|
+
port = find_free_port()
|
|
256
|
+
open_login_page(callback_port=port)
|
|
257
|
+
|
|
258
|
+
# Wait for callback (up to 5 minutes)
|
|
259
|
+
cli_token, error = start_callback_server(port, timeout=300)
|
|
260
|
+
|
|
261
|
+
if error:
|
|
262
|
+
self.app.call_from_thread(
|
|
263
|
+
self.app.notify, f"Login failed: {error}", title="Login", severity="error"
|
|
264
|
+
)
|
|
265
|
+
self.app.call_from_thread(self._update_account_status)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
if not cli_token:
|
|
269
|
+
self.app.call_from_thread(
|
|
270
|
+
self.app.notify, "No token received", title="Login", severity="error"
|
|
271
|
+
)
|
|
272
|
+
self.app.call_from_thread(self._update_account_status)
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
# Validate token
|
|
276
|
+
credentials = validate_cli_token(cli_token)
|
|
277
|
+
if not credentials:
|
|
278
|
+
self.app.call_from_thread(
|
|
279
|
+
self.app.notify, "Invalid token", title="Login", severity="error"
|
|
280
|
+
)
|
|
281
|
+
self.app.call_from_thread(self._update_account_status)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# Save credentials
|
|
285
|
+
if save_credentials(credentials):
|
|
286
|
+
# Sync Supabase uid to local config
|
|
287
|
+
try:
|
|
288
|
+
config = ReAlignConfig.load()
|
|
289
|
+
old_uid = config.uid
|
|
290
|
+
config.uid = credentials.user_id
|
|
291
|
+
if not config.user_name:
|
|
292
|
+
config.user_name = credentials.email.split("@")[0]
|
|
293
|
+
config.save()
|
|
294
|
+
logger.info(f"Synced Supabase uid to config: {credentials.user_id[:8]}...")
|
|
295
|
+
|
|
296
|
+
# V18: Upsert user info to users table
|
|
297
|
+
try:
|
|
298
|
+
from ...db import get_database
|
|
299
|
+
db = get_database()
|
|
300
|
+
db.upsert_user(config.uid, config.user_name)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.debug(f"Failed to upsert user to users table: {e}")
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.warning(f"Failed to sync uid to config: {e}")
|
|
305
|
+
|
|
306
|
+
self.app.call_from_thread(
|
|
307
|
+
self.app.notify, f"Logged in as {credentials.email}", title="Login"
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
self.app.call_from_thread(
|
|
311
|
+
self.app.notify, "Failed to save credentials", title="Login", severity="error"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
self.app.call_from_thread(self._update_account_status)
|
|
315
|
+
|
|
316
|
+
finally:
|
|
317
|
+
self._login_in_progress = False
|
|
318
|
+
|
|
319
|
+
thread = threading.Thread(target=do_login, daemon=True)
|
|
320
|
+
thread.start()
|
|
321
|
+
|
|
322
|
+
self.app.notify("Complete login in browser...", title="Login")
|
|
323
|
+
|
|
324
|
+
def _handle_logout(self) -> None:
|
|
325
|
+
"""Handle logout button click - clear credentials."""
|
|
326
|
+
credentials = load_credentials()
|
|
327
|
+
email = credentials.email if credentials else "user"
|
|
328
|
+
|
|
329
|
+
if clear_credentials():
|
|
330
|
+
self._update_account_status()
|
|
331
|
+
self.app.notify(f"Logged out: {email}", title="Account")
|
|
332
|
+
else:
|
|
333
|
+
self.app.notify("Failed to logout", title="Account", severity="error")
|
|
334
|
+
|
|
159
335
|
def _sync_border_resize_switch(self) -> None:
|
|
160
336
|
"""Query tmux state and sync the switch to match."""
|
|
161
337
|
try:
|
|
@@ -232,6 +232,8 @@ class EventsTable(Container):
|
|
|
232
232
|
table.add_column("Share", key="share", width=12)
|
|
233
233
|
table.add_column("Type", key="type", width=8)
|
|
234
234
|
table.add_column("Sessions", key="sessions", width=8)
|
|
235
|
+
table.add_column("Created By", key="created_by", width=10)
|
|
236
|
+
table.add_column("Shared By", key="shared_by", width=10)
|
|
235
237
|
table.add_column("Event ID", key="event_id", width=12)
|
|
236
238
|
table.add_column("Created", key="created", width=10)
|
|
237
239
|
table.cursor_type = "row"
|
|
@@ -352,6 +354,12 @@ class EventsTable(Container):
|
|
|
352
354
|
table.update_cell(
|
|
353
355
|
eid, "sessions", self._format_cell(str(event["sessions"]), eid)
|
|
354
356
|
)
|
|
357
|
+
table.update_cell(
|
|
358
|
+
eid, "created_by", self._format_cell(event.get("created_by", "-"), eid)
|
|
359
|
+
)
|
|
360
|
+
table.update_cell(
|
|
361
|
+
eid, "shared_by", self._format_cell(event.get("shared_by", "-"), eid)
|
|
362
|
+
)
|
|
355
363
|
table.update_cell(
|
|
356
364
|
eid, "event_id", self._format_cell(event["short_id"], eid)
|
|
357
365
|
)
|
|
@@ -736,6 +744,8 @@ class EventsTable(Container):
|
|
|
736
744
|
e.event_type,
|
|
737
745
|
e.created_at,
|
|
738
746
|
e.share_url,
|
|
747
|
+
e.created_by,
|
|
748
|
+
e.shared_by,
|
|
739
749
|
(SELECT COUNT(*) FROM event_sessions WHERE event_id = e.id) AS session_count
|
|
740
750
|
FROM events e
|
|
741
751
|
ORDER BY e.created_at DESC
|
|
@@ -743,8 +753,9 @@ class EventsTable(Container):
|
|
|
743
753
|
""",
|
|
744
754
|
(int(rows_per_page), int(offset)),
|
|
745
755
|
).fetchall()
|
|
746
|
-
|
|
756
|
+
has_new_columns = True
|
|
747
757
|
except Exception:
|
|
758
|
+
# Fallback for older schema without created_by/shared_by
|
|
748
759
|
rows = conn.execute(
|
|
749
760
|
"""
|
|
750
761
|
SELECT
|
|
@@ -759,18 +770,22 @@ class EventsTable(Container):
|
|
|
759
770
|
""",
|
|
760
771
|
(int(rows_per_page), int(offset)),
|
|
761
772
|
).fetchall()
|
|
762
|
-
|
|
773
|
+
has_new_columns = False
|
|
763
774
|
|
|
764
775
|
for i, row in enumerate(rows):
|
|
765
776
|
event_id = row[0]
|
|
766
777
|
title = row[1] or "(no title)"
|
|
767
778
|
event_type = row[2] or "unknown"
|
|
768
779
|
created_at = row[3]
|
|
769
|
-
if
|
|
780
|
+
if has_new_columns:
|
|
770
781
|
share_url = row[4]
|
|
771
|
-
|
|
782
|
+
created_by = row[5]
|
|
783
|
+
shared_by = row[6]
|
|
784
|
+
session_count = row[7]
|
|
772
785
|
else:
|
|
773
786
|
share_url = None
|
|
787
|
+
created_by = None
|
|
788
|
+
shared_by = None
|
|
774
789
|
session_count = row[4]
|
|
775
790
|
|
|
776
791
|
# Format event type
|
|
@@ -789,6 +804,26 @@ class EventsTable(Container):
|
|
|
789
804
|
except Exception:
|
|
790
805
|
created_str = created_at
|
|
791
806
|
|
|
807
|
+
# Look up user names from users table
|
|
808
|
+
created_by_display = "-"
|
|
809
|
+
shared_by_display = "-"
|
|
810
|
+
if created_by:
|
|
811
|
+
try:
|
|
812
|
+
user_row = conn.execute(
|
|
813
|
+
"SELECT user_name FROM users WHERE uid = ?", (created_by,)
|
|
814
|
+
).fetchone()
|
|
815
|
+
created_by_display = user_row[0] if user_row and user_row[0] else created_by[:8] + "..."
|
|
816
|
+
except Exception:
|
|
817
|
+
created_by_display = created_by[:8] + "..." if len(created_by) > 8 else created_by
|
|
818
|
+
if shared_by:
|
|
819
|
+
try:
|
|
820
|
+
user_row = conn.execute(
|
|
821
|
+
"SELECT user_name FROM users WHERE uid = ?", (shared_by,)
|
|
822
|
+
).fetchone()
|
|
823
|
+
shared_by_display = user_row[0] if user_row and user_row[0] else shared_by[:8] + "..."
|
|
824
|
+
except Exception:
|
|
825
|
+
shared_by_display = shared_by[:8] + "..." if len(shared_by) > 8 else shared_by
|
|
826
|
+
|
|
792
827
|
events.append(
|
|
793
828
|
{
|
|
794
829
|
"index": offset + i + 1,
|
|
@@ -799,6 +834,8 @@ class EventsTable(Container):
|
|
|
799
834
|
"sessions": session_count,
|
|
800
835
|
"share_url": share_url,
|
|
801
836
|
"share_id": self._extract_share_id(share_url),
|
|
837
|
+
"created_by": created_by_display,
|
|
838
|
+
"shared_by": shared_by_display,
|
|
802
839
|
"created": created_str,
|
|
803
840
|
}
|
|
804
841
|
)
|
|
@@ -849,7 +886,7 @@ class EventsTable(Container):
|
|
|
849
886
|
if self.wrap_mode and event.get("share_url"):
|
|
850
887
|
share_val = event.get("share_url")
|
|
851
888
|
|
|
852
|
-
# Column order: ✓, #, Title, Share, Type, Sessions, Event ID, Created
|
|
889
|
+
# Column order: ✓, #, Title, Share, Type, Sessions, Created By, Shared By, Event ID, Created
|
|
853
890
|
table.add_row(
|
|
854
891
|
self._checkbox_cell(eid),
|
|
855
892
|
self._format_cell(str(event["index"]), eid),
|
|
@@ -857,6 +894,8 @@ class EventsTable(Container):
|
|
|
857
894
|
self._format_cell(share_val, eid),
|
|
858
895
|
self._format_cell(event["type"], eid),
|
|
859
896
|
self._format_cell(str(event["sessions"]), eid),
|
|
897
|
+
self._format_cell(event.get("created_by", "-"), eid),
|
|
898
|
+
self._format_cell(event.get("shared_by", "-"), eid),
|
|
860
899
|
self._format_cell(event["short_id"], eid),
|
|
861
900
|
self._format_cell(event["created"], eid),
|
|
862
901
|
key=eid,
|
|
@@ -223,6 +223,8 @@ class SessionsTable(Container):
|
|
|
223
223
|
table.add_column("Project", key="project", width=15)
|
|
224
224
|
table.add_column("Source", key="source", width=10)
|
|
225
225
|
table.add_column("Turns", key="turns", width=6)
|
|
226
|
+
table.add_column("Created By", key="created_by", width=10)
|
|
227
|
+
table.add_column("Shared By", key="shared_by", width=10)
|
|
226
228
|
table.add_column("Session ID", key="session_id", width=20)
|
|
227
229
|
table.add_column("Last Activity", key="last_activity", width=12)
|
|
228
230
|
table.cursor_type = "row"
|
|
@@ -361,6 +363,12 @@ class SessionsTable(Container):
|
|
|
361
363
|
table.update_cell(
|
|
362
364
|
sid, "turns", self._format_cell(str(session["turns"]), sid)
|
|
363
365
|
)
|
|
366
|
+
table.update_cell(
|
|
367
|
+
sid, "created_by", self._format_cell(session.get("created_by", "-"), sid)
|
|
368
|
+
)
|
|
369
|
+
table.update_cell(
|
|
370
|
+
sid, "shared_by", self._format_cell(session.get("shared_by", "-"), sid)
|
|
371
|
+
)
|
|
364
372
|
table.update_cell(
|
|
365
373
|
sid, "session_id", self._format_cell(session["short_id"], sid)
|
|
366
374
|
)
|
|
@@ -805,21 +813,43 @@ class SessionsTable(Container):
|
|
|
805
813
|
# Get paginated sessions
|
|
806
814
|
# Use cached total_turns instead of subquery for performance
|
|
807
815
|
offset = (int(page) - 1) * int(rows_per_page)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
816
|
+
try:
|
|
817
|
+
rows = conn.execute(
|
|
818
|
+
"""
|
|
819
|
+
SELECT
|
|
820
|
+
s.id,
|
|
821
|
+
s.session_type,
|
|
822
|
+
s.workspace_path,
|
|
823
|
+
s.session_title,
|
|
824
|
+
s.last_activity_at,
|
|
825
|
+
s.total_turns,
|
|
826
|
+
s.created_by,
|
|
827
|
+
s.shared_by
|
|
828
|
+
FROM sessions s
|
|
829
|
+
ORDER BY s.last_activity_at DESC
|
|
830
|
+
LIMIT ? OFFSET ?
|
|
831
|
+
""",
|
|
832
|
+
(int(rows_per_page), int(offset)),
|
|
833
|
+
).fetchall()
|
|
834
|
+
has_new_columns = True
|
|
835
|
+
except Exception:
|
|
836
|
+
# Fallback for older schema without created_by/shared_by
|
|
837
|
+
rows = conn.execute(
|
|
838
|
+
"""
|
|
839
|
+
SELECT
|
|
840
|
+
s.id,
|
|
841
|
+
s.session_type,
|
|
842
|
+
s.workspace_path,
|
|
843
|
+
s.session_title,
|
|
844
|
+
s.last_activity_at,
|
|
845
|
+
s.total_turns
|
|
846
|
+
FROM sessions s
|
|
847
|
+
ORDER BY s.last_activity_at DESC
|
|
848
|
+
LIMIT ? OFFSET ?
|
|
849
|
+
""",
|
|
850
|
+
(int(rows_per_page), int(offset)),
|
|
851
|
+
).fetchall()
|
|
852
|
+
has_new_columns = False
|
|
823
853
|
|
|
824
854
|
for i, row in enumerate(rows):
|
|
825
855
|
session_id = row[0]
|
|
@@ -828,6 +858,12 @@ class SessionsTable(Container):
|
|
|
828
858
|
title = row[3] or "(no title)"
|
|
829
859
|
last_activity = row[4]
|
|
830
860
|
turn_count = row[5]
|
|
861
|
+
if has_new_columns:
|
|
862
|
+
created_by = row[6]
|
|
863
|
+
shared_by = row[7]
|
|
864
|
+
else:
|
|
865
|
+
created_by = None
|
|
866
|
+
shared_by = None
|
|
831
867
|
|
|
832
868
|
source_map = {
|
|
833
869
|
"claude": "Claude",
|
|
@@ -846,6 +882,26 @@ class SessionsTable(Container):
|
|
|
846
882
|
except Exception:
|
|
847
883
|
activity_str = last_activity
|
|
848
884
|
|
|
885
|
+
# Look up user names from users table
|
|
886
|
+
created_by_display = "-"
|
|
887
|
+
shared_by_display = "-"
|
|
888
|
+
if created_by:
|
|
889
|
+
try:
|
|
890
|
+
user_row = conn.execute(
|
|
891
|
+
"SELECT user_name FROM users WHERE uid = ?", (created_by,)
|
|
892
|
+
).fetchone()
|
|
893
|
+
created_by_display = user_row[0] if user_row and user_row[0] else created_by[:8] + "..."
|
|
894
|
+
except Exception:
|
|
895
|
+
created_by_display = created_by[:8] + "..." if len(created_by) > 8 else created_by
|
|
896
|
+
if shared_by:
|
|
897
|
+
try:
|
|
898
|
+
user_row = conn.execute(
|
|
899
|
+
"SELECT user_name FROM users WHERE uid = ?", (shared_by,)
|
|
900
|
+
).fetchone()
|
|
901
|
+
shared_by_display = user_row[0] if user_row and user_row[0] else shared_by[:8] + "..."
|
|
902
|
+
except Exception:
|
|
903
|
+
shared_by_display = shared_by[:8] + "..." if len(shared_by) > 8 else shared_by
|
|
904
|
+
|
|
849
905
|
sessions.append(
|
|
850
906
|
{
|
|
851
907
|
"index": offset + i + 1,
|
|
@@ -855,6 +911,8 @@ class SessionsTable(Container):
|
|
|
855
911
|
"project": project,
|
|
856
912
|
"turns": turn_count,
|
|
857
913
|
"title": title,
|
|
914
|
+
"created_by": created_by_display,
|
|
915
|
+
"shared_by": shared_by_display,
|
|
858
916
|
"last_activity": activity_str,
|
|
859
917
|
}
|
|
860
918
|
)
|
|
@@ -905,7 +963,7 @@ class SessionsTable(Container):
|
|
|
905
963
|
if self.wrap_mode:
|
|
906
964
|
display_id = sid
|
|
907
965
|
|
|
908
|
-
# Column order: ✓, #, Title, Project, Source, Turns, Session ID, Last Activity
|
|
966
|
+
# Column order: ✓, #, Title, Project, Source, Turns, Created By, Shared By, Session ID, Last Activity
|
|
909
967
|
table.add_row(
|
|
910
968
|
self._checkbox_cell(sid),
|
|
911
969
|
self._format_cell(str(session["index"]), sid),
|
|
@@ -913,6 +971,8 @@ class SessionsTable(Container):
|
|
|
913
971
|
self._format_cell(session["project"], sid),
|
|
914
972
|
self._format_cell(session["source"], sid),
|
|
915
973
|
self._format_cell(str(session["turns"]), sid),
|
|
974
|
+
self._format_cell(session.get("created_by", "-"), sid),
|
|
975
|
+
self._format_cell(session.get("shared_by", "-"), sid),
|
|
916
976
|
self._format_cell(display_id, sid),
|
|
917
977
|
self._format_cell(session["last_activity"], sid),
|
|
918
978
|
key=sid,
|
realign/db/base.py
CHANGED
|
@@ -49,9 +49,9 @@ class SessionRecord:
|
|
|
49
49
|
summary_status: Optional[str] = None
|
|
50
50
|
summary_locked_until: Optional[datetime] = None
|
|
51
51
|
summary_error: Optional[str] = None
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
# V18: user identity
|
|
53
|
+
created_by: Optional[str] = None # Creator UID
|
|
54
|
+
shared_by: Optional[str] = None # Sharer UID (who imported this)
|
|
55
55
|
temp_title: Optional[str] = None
|
|
56
56
|
# V10: cached total turn count for session list performance
|
|
57
57
|
total_turns: Optional[int] = None
|
|
@@ -78,9 +78,6 @@ class TurnRecord:
|
|
|
78
78
|
git_commit_hash: Optional[str] = None
|
|
79
79
|
# V12: temporary title stored in DB turns.temp_title
|
|
80
80
|
temp_title: Optional[str] = None
|
|
81
|
-
# V9: creator information
|
|
82
|
-
creator_name: Optional[str] = None
|
|
83
|
-
creator_id: Optional[str] = None
|
|
84
81
|
|
|
85
82
|
|
|
86
83
|
@dataclass
|
|
@@ -102,9 +99,9 @@ class EventRecord:
|
|
|
102
99
|
share_id: Optional[str] = None # V14: Share ID on server (for reuse)
|
|
103
100
|
share_admin_token: Optional[str] = None # V14: Admin token for extending expiry
|
|
104
101
|
share_expiry_at: Optional[datetime] = None # V14: Last known expiry timestamp
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
# V18: user identity
|
|
103
|
+
created_by: Optional[str] = None # Creator UID
|
|
104
|
+
shared_by: Optional[str] = None # Sharer UID (who imported this)
|
|
108
105
|
|
|
109
106
|
|
|
110
107
|
@dataclass
|
|
@@ -124,8 +121,8 @@ class AgentRecord:
|
|
|
124
121
|
status: str = "active" # 'active', 'stopped'
|
|
125
122
|
attention: Optional[str] = None # 'permission_request', 'stop', or None
|
|
126
123
|
source: Optional[str] = None
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
# V18: user identity
|
|
125
|
+
created_by: Optional[str] = None # Creator UID
|
|
129
126
|
|
|
130
127
|
|
|
131
128
|
@dataclass
|
|
@@ -143,6 +140,16 @@ class AgentContextRecord:
|
|
|
143
140
|
event_ids: Optional[List[str]] = None
|
|
144
141
|
|
|
145
142
|
|
|
143
|
+
@dataclass
|
|
144
|
+
class UserRecord:
|
|
145
|
+
"""Represents a user in the users table (V18)."""
|
|
146
|
+
|
|
147
|
+
uid: str
|
|
148
|
+
user_name: Optional[str] = None
|
|
149
|
+
created_at: Optional[datetime] = None
|
|
150
|
+
updated_at: Optional[datetime] = None
|
|
151
|
+
|
|
152
|
+
|
|
146
153
|
class DatabaseInterface(ABC):
|
|
147
154
|
"""Abstract interface for ReAlign storage backend."""
|
|
148
155
|
|
|
@@ -420,3 +427,13 @@ class DatabaseInterface(ABC):
|
|
|
420
427
|
def get_all_locks(self, include_expired: bool = False) -> List[LockRecord]:
|
|
421
428
|
"""Get all locks, optionally including expired ones."""
|
|
422
429
|
pass
|
|
430
|
+
|
|
431
|
+
@abstractmethod
|
|
432
|
+
def upsert_user(self, uid: str, user_name: Optional[str] = None) -> None:
|
|
433
|
+
"""Insert or update a user in the users table (V18)."""
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
@abstractmethod
|
|
437
|
+
def get_user(self, uid: str) -> Optional[UserRecord]:
|
|
438
|
+
"""Get a user by UID from the users table (V18)."""
|
|
439
|
+
pass
|