conductor-remote 1.62.2 → 1.64.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
@@ -13,7 +13,7 @@
13
13
  <title>Conductor Remote</title>
14
14
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
15
15
  <script src="/self-heal.js"></script>
16
- <script type="module" crossorigin src="/assets/index-DbiS5Osj.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-C8s68scA.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-BhxPTIyU.css">
18
18
  <link rel="manifest" href="/manifest.webmanifest"></head>
19
19
  <body>
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"574d3c26604d0b89b022cb72ee047d00"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DbiS5Osj.js",revision:null},{url:"assets/index-BhxPTIyU.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\//]}))});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const c=e=>i(e,o),t={module:{uri:o},exports:l,require:c};s[o]=Promise.all(n.map(e=>t[e]||c(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"0cc53b4390ac109ed647b9f747869331"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-C8s68scA.js",revision:null},{url:"assets/index-BhxPTIyU.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\//]}))});
@@ -116,7 +116,10 @@ export function createTools(call) {
116
116
  inputSchema: {
117
117
  type: 'object',
118
118
  properties: {
119
- query: { type: 'string', description: 'Plain words. Punctuation and operators are ignored, not parsed.' },
119
+ query: {
120
+ type: 'string',
121
+ description: 'Plain words, ranked by relevance; wrap a "phrase in quotes" to require it word for word. Other punctuation and operators are ignored, not parsed.'
122
+ },
120
123
  limit: { type: 'number', description: 'Max workspaces to return (default 12, max 50).' }
121
124
  },
122
125
  required: ['query']
@@ -132,15 +132,29 @@ export class Reads {
132
132
  return this.listWorkspaces().find(w => w.id === id) ?? null;
133
133
  }
134
134
  /**
135
- * The repos Conductor knows about, in its own sidebar order. `root_path` is
135
+ * The repos Conductor knows about, most recently used first. `root_path` is
136
136
  * what a `conductor://…&path=` deep link needs to pick the target repo — with
137
137
  * no path Conductor silently falls back to the *first* repo.
138
+ *
139
+ * "Used" is the newest workspace in the repo, never `repos.updated_at`: that
140
+ * column moves when the repo's *settings* change, so the repo worked in every
141
+ * day here reads as last touched six weeks ago. Repos with no workspace at all
142
+ * keep Conductor's own sidebar order, behind every repo that has one.
143
+ *
144
+ * The `replace()`s are load-bearing: `created_at` is always `YYYY-MM-DD HH:MM:SS`
145
+ * while `updated_at` is written both that way (306 rows) and as ISO with a `Z`
146
+ * (1,819), and 'T' sorts after ' ', so an untouched string compare can rank an
147
+ * earlier ISO row above a later plain one on the same day.
138
148
  */
139
149
  listRepos() {
140
- const rows = this.db.query(`SELECT name, root_path, default_branch, icon, remote_url
141
- FROM repos
142
- WHERE COALESCE(hidden, 0) = 0
143
- ORDER BY (display_order IS NULL), display_order, name`);
150
+ const rows = this.db.query(`SELECT r.name, r.root_path, r.default_branch, r.icon, r.remote_url
151
+ FROM repos r
152
+ LEFT JOIN workspaces w ON w.repository_id = r.id
153
+ WHERE COALESCE(r.hidden, 0) = 0
154
+ GROUP BY r.id
155
+ ORDER BY (MAX(MAX(REPLACE(REPLACE(w.updated_at, 'T', ' '), 'Z', ''), w.created_at)) IS NULL),
156
+ MAX(MAX(REPLACE(REPLACE(w.updated_at, 'T', ' '), 'Z', ''), w.created_at)) DESC,
157
+ (r.display_order IS NULL), r.display_order, r.name`);
144
158
  return rows.map(r => ({
145
159
  name: r.name,
146
160
  root_path: r.root_path,
@@ -80,19 +80,58 @@ const INDEXED_ROLES = new Set(['user', 'assistant', 'thinking']);
80
80
  * the one matching one, and requiring all four would return nothing whenever a
81
81
  * single word is misremembered.
82
82
  *
83
+ * OR alone loses the query made of common words, and nothing about sentence
84
+ * boundaries is why — a chunk is a whole prose block. Measured 2026-09-01 against
85
+ * this Mac's 225k-chunk index: "may i run the", a sentence one chat verifiably
86
+ * contains, ranked nowhere in the top 300 chunks, because OR rewards density
87
+ * ("Running the headless artifact render… first run may…") and nothing rewards
88
+ * adjacency. So each run of unquoted words also enters as one FTS5 phrase term
89
+ * OR'd beside its tokens: the phrase is rare where the words are common, its BM25
90
+ * weight is correspondingly large, and the same query then puts the exact sentence
91
+ * first (95ms against 206ms for bare OR, both inside the 250ms debounce). A phrase
92
+ * the index doesn't contain matches nothing and costs nothing.
93
+ *
94
+ * Quotes filter. `"exactly this"` becomes a *required* phrase (AND), because a
95
+ * typed quote is the one signal that the user is filtering rather than recalling;
96
+ * words outside the quotes stay OR'd. Curly `“”` count as quotes — iOS smart
97
+ * punctuation substitutes them for the straight one the user pressed, so the phone
98
+ * mostly never sends `"`. Stemming still applies inside quotes (`"running"` finds
99
+ * "runs"): quoting changes which chunks qualify, not how they were tokenized.
100
+ *
83
101
  * The last token gets a prefix `*` so search-as-you-type matches mid-word, but only
84
102
  * from three characters: `"a"*` matches a large fraction of the index and would
85
- * spend the whole query budget on a keystroke that means nothing yet.
103
+ * spend the whole query budget on a keystroke that means nothing yet. Inside an
104
+ * unclosed quote the star rides on the phrase itself (`"may i ru"*` — valid FTS5,
105
+ * the last token matches as a prefix), so a phrase still being typed already
106
+ * matches what it is about to say.
86
107
  */
87
108
  export function matchQuery(raw) {
88
- const tokens = raw.toLowerCase().match(/[\p{L}\p{N}_]+/gu);
89
- if (!tokens?.length)
90
- return null;
91
- const terms = tokens.map(t => `"${t}"`);
92
- const last = tokens[tokens.length - 1];
93
- if (!/\s$/.test(raw) && last.length >= 3)
94
- terms[terms.length - 1] = `"${last}"*`;
95
- return terms.join(' OR ');
109
+ // Split on quote characters: segments alternate unquoted/quoted, and a trailing
110
+ // unclosed quote leaves the last segment quoted — the phrase still being typed.
111
+ const segments = raw.toLowerCase().split(/["“”]/);
112
+ const typing = !/[\s"“”]$/.test(raw);
113
+ const required = [];
114
+ const loose = [];
115
+ for (let i = 0; i < segments.length; i++) {
116
+ const tokens = segments[i].match(/[\p{L}\p{N}_]+/gu);
117
+ if (!tokens?.length)
118
+ continue;
119
+ const star = i === segments.length - 1 && typing && tokens[tokens.length - 1].length >= 3 ? '*' : '';
120
+ const phrase = `"${tokens.join(' ')}"${star}`;
121
+ if (i % 2) {
122
+ required.push(phrase);
123
+ }
124
+ else {
125
+ if (tokens.length > 1)
126
+ loose.push(phrase);
127
+ loose.push(...tokens.slice(0, -1).map(t => `"${t}"`), `"${tokens[tokens.length - 1]}"${star}`);
128
+ }
129
+ }
130
+ if (!loose.length)
131
+ return required.length ? required.join(' AND ') : null;
132
+ // The OR group is parenthesised because FTS5 binds AND tighter than OR.
133
+ const looseExpr = loose.length > 1 ? `(${loose.join(' OR ')})` : loose[0];
134
+ return required.length ? `${required.join(' AND ')} AND ${looseExpr}` : looseExpr;
96
135
  }
97
136
  /** The tokens `matchQuery` will search for — what a caller matches names against. */
98
137
  export { queryTokens } from "./shared.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.62.2",
3
+ "version": "1.64.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.",