Skip to content

Commit c76608c

Browse files
sarth6claude
andcommitted
Fix Conductor deep link URL: it is conductor://prompt=… not conductor://new?prompt=…
The previous default URL template was a best-guess derived from the Conductor changelog (v0.36.4 mentions "prompt and path parameters" for the Linear integration). It turns out the actual working format is structurally unusual: conductor://prompt=<percent-encoded text> No host, no `?` — the `prompt=…` sits directly after `://`. This was discovered the hard way in slack-flow: an earlier Claude session there tested ~25 candidate formats (conductor://new?prompt=, conductor://open?prompt=, conductor://workspace?prompt=, etc.) before landing on this one. The committed slack-flow ConductorBridge.swift uses exactly this format. Changes: - DEFAULT_SETTINGS.urlConfig.template flipped to conductor://prompt={prompt} - mergeWithDefaults() migrates any stored URL on a known-broken list to the new default, so users who already saved settings with the previous bad default get auto-fixed on next load (no manual reset). - conductor-url tests updated to assert the new format. - Two new storage tests covering migration + preservation of custom templates. - README documents the unusual shape with an explicit "don't be fooled" note. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 616f8cc commit c76608c

5 files changed

Lines changed: 68 additions & 34 deletions

File tree

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,17 @@ Right-click the toolbar icon → **Options** (or use the gear in the popup).
5151
Sets the `conductor://` URL opened on click. The default is:
5252

5353
```
54-
conductor://new?prompt={prompt}
54+
conductor://prompt={prompt}
5555
```
5656

57-
`{prompt}` is replaced with the URL-encoded rendered prompt. You can also use
58-
PR metadata placeholders directly in the URL (e.g. to set the workspace path):
57+
> **Note on Conductor's unusual URL shape**: the working format has the
58+
> prompt sitting directly after `://` — no host, no `?`. It is not
59+
> `conductor://new?prompt=…` or `conductor://?prompt=…`. The Conductor app
60+
> looks for `prompt=` at the start of the URL body. This format was
61+
> empirically verified across ~25 candidate URLs.
5962
60-
```
61-
conductor://new?prompt={prompt}&path=/Users/me/code/{repoName}
62-
```
63+
`{prompt}` is replaced with the URL-encoded rendered prompt. You can also use
64+
PR metadata placeholders directly in the URL template.
6365

6466
### Prompt presets
6567

src/conductor-url.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ import { renderTemplate } from './template';
1313
* URL-encoded prompt. Other PR metadata placeholders in the URL template
1414
* are also URL-encoded.
1515
*
16-
* This keeps the URL template authorable (`conductor://new?prompt={prompt}`)
16+
* This keeps the URL template authorable (`conductor://prompt={prompt}`)
1717
* while preventing malformed URLs when the PR title contains `&`, `=`, `#`,
1818
* spaces, newlines, etc.
19+
*
20+
* Note on Conductor's unusual URL shape: the working format is
21+
* `conductor://prompt=<encoded text>`
22+
* — no host, no `?`, the `prompt=…` sits directly after `://`. We still URL-
23+
* encode the text the same way, but the structural template differs from a
24+
* conventional `scheme://host?key=value` URL.
1925
*/
2026
export function buildConductorUrl(
2127
urlConfig: ConductorUrlConfig,

src/storage.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@ export const DEFAULT_SETTINGS: Settings = {
3737
],
3838
defaultPresetId: 'review',
3939
urlConfig: {
40-
// Default to the format Conductor's Linear integration uses (changelog v0.36.4).
41-
// The full prompt is passed in the `prompt` query param.
42-
template: 'conductor://new?prompt={prompt}',
40+
// Conductor's deep link is structurally unusual: the prompt sits directly
41+
// after `://` with no host or query string — `conductor://prompt=<text>`,
42+
// NOT `conductor://new?prompt=…` or `conductor://?prompt=…`.
43+
// Empirically verified across ~25 candidate formats; this is the only one
44+
// that triggers the workspace creation flow.
45+
template: 'conductor://prompt={prompt}',
4346
},
4447
};
4548

@@ -103,18 +106,36 @@ class MemoryStorageAdapter implements StorageAdapter {
103106
}
104107
}
105108

109+
/**
110+
* URL templates we shipped in older versions that turned out to be broken
111+
* (Conductor's actual deep link is `conductor://prompt=…`, not any
112+
* `conductor://…?prompt=…` form). When we see one of these stored, we
113+
* silently migrate to the current default. Listed exactly so we never
114+
* touch a user's intentional customization that happens to look similar.
115+
*/
116+
const KNOWN_BROKEN_URL_TEMPLATES = new Set<string>([
117+
'conductor://new?prompt={prompt}',
118+
'conductor://?prompt={prompt}',
119+
'conductor://open?prompt={prompt}',
120+
'conductor://workspace?prompt={prompt}',
121+
]);
122+
106123
/**
107124
* Merge any partial stored settings with defaults so new fields added in
108-
* future versions are populated without wiping user data.
125+
* future versions are populated without wiping user data. Also migrates
126+
* known-broken URL templates to the current default.
109127
*/
110128
export function mergeWithDefaults(stored: Partial<Settings> | undefined): Settings {
111129
if (!stored) return structuredClone(DEFAULT_SETTINGS);
130+
const storedTemplate = stored.urlConfig?.template;
131+
const template =
132+
storedTemplate && !KNOWN_BROKEN_URL_TEMPLATES.has(storedTemplate)
133+
? storedTemplate
134+
: DEFAULT_SETTINGS.urlConfig.template;
112135
return {
113136
presets: stored.presets ?? DEFAULT_SETTINGS.presets,
114137
defaultPresetId: stored.defaultPresetId ?? DEFAULT_SETTINGS.defaultPresetId,
115-
urlConfig: {
116-
template: stored.urlConfig?.template ?? DEFAULT_SETTINGS.urlConfig.template,
117-
},
138+
urlConfig: { template },
118139
};
119140
}
120141

tests/conductor-url.test.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,18 @@ const fixture: PRMetadata = {
1818
};
1919

2020
describe('buildConductorUrl', () => {
21-
it('builds the default conductor://new?prompt=... URL', () => {
21+
it('builds the default conductor://prompt= URL (no host, no ?)', () => {
2222
const url = buildConductorUrl(
23-
{ template: 'conductor://new?prompt={prompt}' },
23+
{ template: 'conductor://prompt={prompt}' },
2424
'Review PR: {prTitle}',
2525
fixture,
2626
);
27-
expect(url).toBe(
28-
`conductor://new?prompt=${encodeURIComponent('Review PR: Fix bug & add tests')}`,
29-
);
27+
expect(url).toBe(`conductor://prompt=${encodeURIComponent('Review PR: Fix bug & add tests')}`);
3028
});
3129

3230
it('URL-encodes special characters in the rendered prompt', () => {
3331
const url = buildConductorUrl(
34-
{ template: 'conductor://new?prompt={prompt}' },
32+
{ template: 'conductor://prompt={prompt}' },
3533
'{prTitle} & {prAuthor}',
3634
fixture,
3735
);
@@ -40,24 +38,13 @@ describe('buildConductorUrl', () => {
4038
});
4139

4240
it('encodes PR metadata placeholders used directly in the URL template', () => {
41+
// Legacy ?prompt=…&path=… style still supported for users who want it.
4342
const url = buildConductorUrl(
44-
{ template: 'conductor://new?prompt={prompt}&path={repo}' },
43+
{ template: 'conductor://prompt={prompt}&path={repo}' },
4544
'go',
4645
fixture,
4746
);
48-
expect(url).toBe('conductor://new?prompt=go&path=octocat%2Fhello');
49-
});
50-
51-
it('preserves the path query param example from the Conductor changelog', () => {
52-
// Conductor v0.36.4: "handle prompt and path parameters"
53-
const url = buildConductorUrl(
54-
{ template: 'conductor://new?prompt={prompt}&path=/Users/me/code/{repoName}' },
55-
'Look at #{prNumber}',
56-
fixture,
57-
);
58-
expect(url).toBe(
59-
`conductor://new?prompt=${encodeURIComponent('Look at #42')}&path=/Users/me/code/hello`,
60-
);
47+
expect(url).toBe('conductor://prompt=go&path=octocat%2Fhello');
6148
});
6249
});
6350

tests/storage.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ describe('mergeWithDefaults', () => {
2121
expect(merged.urlConfig.template).toBe(DEFAULT_SETTINGS.urlConfig.template);
2222
});
2323

24+
it('migrates known-broken URL templates to the working default', () => {
25+
for (const broken of [
26+
'conductor://new?prompt={prompt}',
27+
'conductor://?prompt={prompt}',
28+
'conductor://open?prompt={prompt}',
29+
'conductor://workspace?prompt={prompt}',
30+
]) {
31+
const merged = mergeWithDefaults({ urlConfig: { template: broken } });
32+
expect(merged.urlConfig.template).toBe('conductor://prompt={prompt}');
33+
}
34+
});
35+
36+
it('preserves a custom URL template that is not on the known-broken list', () => {
37+
const custom = 'conductor://prompt={prompt}&path=/Users/me/code/{repoName}';
38+
const merged = mergeWithDefaults({ urlConfig: { template: custom } });
39+
expect(merged.urlConfig.template).toBe(custom);
40+
});
41+
2442
it('returns a deep clone so callers cannot mutate defaults', () => {
2543
const a = mergeWithDefaults(undefined);
2644
const b = mergeWithDefaults(undefined);

0 commit comments

Comments
 (0)