conductor-remote 1.2.1 → 1.4.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/dist/index.html CHANGED
@@ -11,8 +11,8 @@
11
11
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
12
12
  <link rel="icon" type="image/svg+xml" href="/icon.svg" />
13
13
  <title>Conductor Remote</title>
14
- <script type="module" crossorigin src="/assets/index-DEhQr8iI.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-DiiwvzTm.css">
14
+ <script type="module" crossorigin src="/assets/index-Dza-b9zD.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BcgS8jNR.css">
16
16
  <link rel="manifest" href="/manifest.webmanifest"></head>
17
17
  <body>
18
18
  <div id="root"></div>
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(i[o])return;let t={};const l=e=>n(e,o),c={module:{uri:o},exports:t,require:l};i[o]=Promise.all(s.map(e=>c[e]||l(e))).then(e=>(r(...e),t))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"9a4082395ee3d7b564ee502d6b9576fc"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DiiwvzTm.css",revision:null},{url:"assets/index-DEhQr8iI.js",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
1
+ if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(i[r])return;let c={};const t=e=>n(e,r),l={module:{uri:r},exports:c,require:t};i[r]=Promise.all(s.map(e=>l[e]||t(e))).then(e=>(o(...e),c))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"679c71c87637a7da381d770ba23bd4e3"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-Dza-b9zD.js",revision:null},{url:"assets/index-BcgS8jNR.css",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
package/src/icons.ts ADDED
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ /**
5
+ * Repo-icon resolution, mirroring Conductor's own logic so the phone sidebar shows
6
+ * the same avatar as the desktop app. Conductor looks for a known icon filename in
7
+ * the repository root and uses the first match — see
8
+ * https://www.conductor.build/docs/faq#where-does-conductor-get-the-repo-icon
9
+ *
10
+ * This reads from the repo's root checkout (the shared, canonical tree), not a
11
+ * per-workspace worktree, so every workspace of a repo resolves to one icon.
12
+ */
13
+ const ICON_CANDIDATES = [
14
+ 'public/apple-touch-icon.png',
15
+ 'apple-touch-icon.png',
16
+ 'public/favicon.svg',
17
+ 'favicon.svg',
18
+ 'public/favicon.png',
19
+ 'public/icon.png',
20
+ 'public/logo.png',
21
+ 'favicon.png',
22
+ 'app/icon.png',
23
+ 'src/app/icon.png',
24
+ 'public/favicon.ico',
25
+ 'favicon.ico',
26
+ 'app/favicon.ico',
27
+ 'static/favicon.ico',
28
+ 'src-tauri/icons/icon.png',
29
+ 'assets/icon.png',
30
+ 'src/assets/icon.png'
31
+ ]
32
+
33
+ const CONTENT_TYPES: Record<string, string> = {
34
+ '.png': 'image/png',
35
+ '.svg': 'image/svg+xml',
36
+ '.ico': 'image/x-icon'
37
+ }
38
+
39
+ export interface ResolvedIcon {
40
+ /** Absolute path to the icon file on disk. */
41
+ path: string
42
+ contentType: string
43
+ }
44
+
45
+ // Icons rarely change; a short TTL keeps the 2.5s state poll from stat-storming the
46
+ // disk while still picking up a freshly-added icon within a tick or two.
47
+ const TTL_MS = 30_000
48
+ const cache = new Map<string, { at: number; icon: ResolvedIcon | null }>()
49
+
50
+ /** First matching icon under `repoRoot`, or null if the repo has none. Cached per root. */
51
+ export function resolveRepoIcon(repoRoot: string): ResolvedIcon | null {
52
+ const now = Date.now()
53
+ const hit = cache.get(repoRoot)
54
+ if (hit && now - hit.at < TTL_MS) return hit.icon
55
+
56
+ let icon: ResolvedIcon | null = null
57
+ for (const rel of ICON_CANDIDATES) {
58
+ const abs = path.join(repoRoot, rel)
59
+ if (fs.existsSync(abs)) {
60
+ icon = { path: abs, contentType: CONTENT_TYPES[path.extname(abs).toLowerCase()] ?? 'application/octet-stream' }
61
+ break
62
+ }
63
+ }
64
+ cache.set(repoRoot, { at: now, icon })
65
+ return icon
66
+ }
package/src/reads.ts CHANGED
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import type { ConductorDb } from './db.ts'
5
+ import { type ResolvedIcon, resolveRepoIcon } from './icons.ts'
5
6
  import { parseMessage, type TranscriptEntry } from './transcript.ts'
6
7
 
7
8
  export interface WorkspaceRow {
@@ -11,6 +12,7 @@ export interface WorkspaceRow {
11
12
  branch: string | null
12
13
  derived_status: string | null
13
14
  manual_status: string | null
15
+ created_at: string
14
16
  updated_at: string
15
17
  unread: number | null
16
18
  pinned_at: string | null
@@ -42,6 +44,8 @@ export interface Workspace extends WorkspaceRow {
42
44
  /** Absolute path to the git worktree on disk, or null if it can't be resolved. */
43
45
  worktree: string | null
44
46
  baseBranch: string
47
+ /** Whether the repo has a resolvable icon (fetch it from `/api/repos/:name/icon`). */
48
+ hasRepoIcon: boolean
45
49
  }
46
50
 
47
51
  const worktreeCache = new Map<string, string | null>()
@@ -101,7 +105,7 @@ export class Reads {
101
105
  listWorkspaces(): Workspace[] {
102
106
  const rows = this.db.query<WorkspaceRow>(
103
107
  `SELECT w.id, w.directory_name, w.workspace_name, w.branch, w.derived_status, w.manual_status,
104
- w.updated_at, w.unread, w.pinned_at, w.active_session_id, w.intended_target_branch,
108
+ w.created_at, w.updated_at, w.unread, w.pinned_at, w.active_session_id, w.intended_target_branch,
105
109
  r.name AS repo_name, r.root_path AS repo_root, r.default_branch AS default_branch,
106
110
  s.status AS session_status, s.title AS session_title, s.model AS model,
107
111
  s.context_used_percent AS context_used_percent
@@ -114,7 +118,8 @@ export class Reads {
114
118
  return rows.map(r => ({
115
119
  ...r,
116
120
  worktree: resolveWorktree(this.workspacesRoot, r.repo_name, r.directory_name, r.branch, r.repo_root),
117
- baseBranch: r.intended_target_branch || r.default_branch || 'main'
121
+ baseBranch: r.intended_target_branch || r.default_branch || 'main',
122
+ hasRepoIcon: !!(r.repo_root && resolveRepoIcon(r.repo_root))
118
123
  }))
119
124
  }
120
125
 
@@ -122,6 +127,15 @@ export class Reads {
122
127
  return this.listWorkspaces().find(w => w.id === id) ?? null
123
128
  }
124
129
 
130
+ /** Resolve a repo's icon by its name (the sidebar avatar) — null if the repo or icon is unknown. */
131
+ resolveRepoIcon(repoName: string): ResolvedIcon | null {
132
+ const rows = this.db.query<{ root_path: string | null }>('SELECT root_path FROM repos WHERE name = ? LIMIT 1', [
133
+ repoName
134
+ ])
135
+ const root = rows[0]?.root_path
136
+ return root ? resolveRepoIcon(root) : null
137
+ }
138
+
125
139
  listSessions(workspaceId: string): SessionRow[] {
126
140
  // created_at ASC keeps tab order stable (matches the desktop app) instead of jumping on activity.
127
141
  return this.db.query<SessionRow>(
package/src/server.ts CHANGED
@@ -98,8 +98,21 @@ const server = http.createServer(async (req, res) => {
98
98
  })
99
99
  }
100
100
 
101
+ // GET /api/repos/:name/icon — the repo's resolved sidebar icon (see src/icons.ts)
102
+ let m = pathname.match(/^\/api\/repos\/([^/]+)\/icon$/)
103
+ if (req.method === 'GET' && m) {
104
+ const icon = reads.resolveRepoIcon(decodeURIComponent(m[1]))
105
+ if (!icon) return json(res, 404, { error: 'no icon' })
106
+ return void fs.readFile(icon.path, (err, data) => {
107
+ if (err) return void json(res, 404, { error: 'no icon' })
108
+ // Cache briefly on the phone; the resolver itself refreshes within ~30s of an icon change.
109
+ res.writeHead(200, { 'content-type': icon.contentType, 'cache-control': 'public, max-age=300' })
110
+ res.end(data)
111
+ })
112
+ }
113
+
101
114
  // GET /api/workspaces/:id/sessions
102
- let m = pathname.match(/^\/api\/workspaces\/([^/]+)\/sessions$/)
115
+ m = pathname.match(/^\/api\/workspaces\/([^/]+)\/sessions$/)
103
116
  if (req.method === 'GET' && m) {
104
117
  return json(res, 200, { sessions: reads.listSessions(decodeURIComponent(m[1])) })
105
118
  }