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/assets/index-BcgS8jNR.css +1 -0
- package/dist/assets/index-Dza-b9zD.js +40 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/src/icons.ts +66 -0
- package/src/reads.ts +16 -2
- package/src/server.ts +14 -1
- package/dist/assets/index-DEhQr8iI.js +0 -40
- package/dist/assets/index-DiiwvzTm.css +0 -1
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-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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,
|
|
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
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
|
-
|
|
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
|
}
|