diff --git a/frontend/src/bindings/github.com/illegalstudio/lazyagent/internal/tray/models.ts b/frontend/src/bindings/github.com/illegalstudio/lazyagent/internal/tray/models.ts index eff21fd..599cce7 100644 --- a/frontend/src/bindings/github.com/illegalstudio/lazyagent/internal/tray/models.ts +++ b/frontend/src/bindings/github.com/illegalstudio/lazyagent/internal/tray/models.ts @@ -77,6 +77,7 @@ export class SessionFull { "desktopTitle"?: string; "desktopId"?: string; "permissionMode"?: string; + "remoteUrl"?: string; /** Creates a new SessionFull instance. */ constructor($$source: Partial = {}) { diff --git a/frontend/src/lib/SessionDetail.svelte b/frontend/src/lib/SessionDetail.svelte index 6ca611f..08166f8 100644 --- a/frontend/src/lib/SessionDetail.svelte +++ b/frontend/src/lib/SessionDetail.svelte @@ -170,6 +170,14 @@
Worktree
yes {#if detail.mainRepo}({detail.mainRepo}){/if}
{/if} + {#if detail.remoteUrl} +
Remote
+
+ + {detail.remoteUrl} + +
+ {/if} {#if detail.lastFileWrite}
Last file
diff --git a/internal/api/server.go b/internal/api/server.go index c615340..e9f098c 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -375,6 +375,7 @@ type SessionFull struct { DesktopTitle string `json:"desktop_title,omitempty"` DesktopID string `json:"desktop_id,omitempty"` PermissionMode string `json:"permission_mode,omitempty"` + RemoteURL string `json:"remote_url,omitempty"` } // ToolItem represents a recent tool call. @@ -464,6 +465,7 @@ func (s *Server) buildSessionFull(detail *core.SessionDetailView) SessionFull { DesktopTitle: desktopTitle(sess), DesktopID: desktopID(sess), PermissionMode: desktopPermission(sess), + RemoteURL: sess.RemoteURL, } } diff --git a/internal/claude/jsonl.go b/internal/claude/jsonl.go index 2c67190..683c25e 100644 --- a/internal/claude/jsonl.go +++ b/internal/claude/jsonl.go @@ -19,6 +19,8 @@ import ( // struct allocation entirely. type jsonlEntry struct { Type string `json:"type"` + Subtype string `json:"subtype"` + URL string `json:"url"` SessionID string `json:"sessionId"` CWD string `json:"cwd"` Version string `json:"version"` @@ -125,6 +127,10 @@ func scanEntries(scanner *bufio.Scanner, session *model.Session, initialOffset i } } + if e.Type == "system" && e.Subtype == "bridge_status" && e.URL != "" { + session.RemoteURL = e.URL + } + if !ts.IsZero() { prevTimestamp = ts entryTimestamps = append(entryTimestamps, ts) diff --git a/internal/claude/jsonl_test.go b/internal/claude/jsonl_test.go index 776df08..7df34ab 100644 --- a/internal/claude/jsonl_test.go +++ b/internal/claude/jsonl_test.go @@ -716,3 +716,36 @@ func TestCopyEntry(t *testing.T) { t.Error("modifying copy should not affect original") } } + +func TestParseJSONL_BridgeStatus(t *testing.T) { + content := mkJSONL( + `{"type":"system","subtype":"bridge_status","url":"https://claude.ai/code/session_ABC123","sessionId":"abc","version":"2.1.86","timestamp":"2026-01-15T10:00:00Z"}`, + `{"type":"user","timestamp":"2026-01-15T10:00:01Z","message":{"role":"user","content":"hello"},"cwd":"/proj","version":"1.0","gitBranch":"main"}`, + `{"type":"assistant","timestamp":"2026-01-15T10:00:05Z","message":{"role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Hi!"}],"usage":{"input_tokens":10,"output_tokens":5}}}`, + ) + path := writeTempJSONL(t, content) + + sess, _, err := ParseJSONL(path) + if err != nil { + t.Fatal(err) + } + if sess.RemoteURL != "https://claude.ai/code/session_ABC123" { + t.Errorf("RemoteURL = %q, want %q", sess.RemoteURL, "https://claude.ai/code/session_ABC123") + } +} + +func TestParseJSONL_NoBridgeStatus(t *testing.T) { + content := mkJSONL( + `{"type":"user","timestamp":"2026-01-15T10:00:01Z","message":{"role":"user","content":"hello"},"cwd":"/proj","version":"1.0","gitBranch":"main"}`, + `{"type":"assistant","timestamp":"2026-01-15T10:00:05Z","message":{"role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Hi!"}],"usage":{"input_tokens":10,"output_tokens":5}}}`, + ) + path := writeTempJSONL(t, content) + + sess, _, err := ParseJSONL(path) + if err != nil { + t.Fatal(err) + } + if sess.RemoteURL != "" { + t.Errorf("RemoteURL = %q, want empty", sess.RemoteURL) + } +} diff --git a/internal/model/types.go b/internal/model/types.go index ec00c57..c90fe1e 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -100,6 +100,9 @@ type Session struct { // Desktop metadata (non-nil if session was started via Claude Desktop) Desktop *DesktopMeta + + // Remote control (Claude Code only, empty if unavailable) + RemoteURL string } // Clone returns a deep copy of the Session suitable for use as a base diff --git a/internal/tray/service.go b/internal/tray/service.go index 1a0be2c..2a9e4d6 100644 --- a/internal/tray/service.go +++ b/internal/tray/service.go @@ -152,6 +152,7 @@ type SessionFull struct { DesktopTitle string `json:"desktopTitle,omitempty"` DesktopID string `json:"desktopId,omitempty"` PermissionMode string `json:"permissionMode,omitempty"` + RemoteURL string `json:"remoteUrl,omitempty"` } // ToolItem is a tool call for the detail view. @@ -253,6 +254,7 @@ func (s *SessionService) GetSessionDetail(id string) *SessionFull { full.DesktopID = sess.Desktop.DesktopID full.PermissionMode = sess.Desktop.PermissionMode } + full.RemoteURL = sess.RemoteURL return full } diff --git a/internal/ui/app.go b/internal/ui/app.go index 81c7c5e..1815172 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -60,6 +60,9 @@ type Model struct { // Flash message (modal popup, dismissed by any key) flashMsg string + // Inline "copied!" indicator for remote URL + copiedAt time.Time + // Update notification shown in footer updateVersion string @@ -494,6 +497,17 @@ func (m *Model) handleMouse(msg tea.MouseMsg) { } } else { m.focus = 1 + // Copy remote URL to clipboard on detail panel click. + if m.cursor >= 0 && m.cursor < len(m.visible) { + if u := m.visible[m.cursor].RemoteURL; u != "" { + if cmd := exec.Command("pbcopy"); cmd != nil { + cmd.Stdin = strings.NewReader(u) + if cmd.Run() == nil { + m.copiedAt = time.Now() + } + } + } + } } } } @@ -934,6 +948,13 @@ func (m Model) buildDetailLines(s *model.Session, width int) []string { if s.GitBranch != "" && s.GitBranch != "HEAD" { add(row("Git Branch", s.GitBranch)) } + if s.RemoteURL != "" { + remoteVal := lipgloss.NewStyle().Foreground(colorAccent).Render(s.RemoteURL) + if time.Since(m.copiedAt) < 2*time.Second { + remoteVal += lipgloss.NewStyle().Foreground(colorMuted).Render(" copied!") + } + add(row("Remote", remoteVal)) + } wtStr := "no" if s.IsWorktree {