Skip to content

Commit fc34c98

Browse files
committed
Merge remote-tracking branch 'origin/main' into deps-low-risk-batch
# Conflicts: # frontend/package-lock.json # frontend/package.json
2 parents 6652cb9 + 6dea0f7 commit fc34c98

30 files changed

Lines changed: 1516 additions & 140 deletions

.claude/rules/testing.md

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,29 @@ layer per capability," never "a seed per thing":
154154
poll. The earlier headless-windowing lead was never actually
155155
tested — the harness failed before the app could boot. With
156156
`syscall.Signal(0)` the local run connects and drives the real
157-
registry (the click-model and selection-ring checks have passed
158-
against the real WKWebView). What remains before the job means
159-
anything: the CHECK REGISTRY was written blind against a
160-
single-window assumption — Mill's desktop build opens multiple
161-
windows (main + the Quick Panel's second window), so
162-
`app-info-window-sane` and the bridge's window-ambiguous clicks
163-
and badge locator need calibrating against the app's real window
164-
shape (tracked as its own goal). Until that lands, the OPERATIVE
165-
parity gate remains the LOCAL run, read with the registry's
166-
current miscalibration in mind; the CI job stays wired,
167-
non-required.
157+
registry. **Calibrated (goal 0107): all six checks green, three
158+
consecutive local runs.** The registry now asserts the app's real
159+
three-window shape by NAME (main + quickpanel + approvalprompt;
160+
the main window carries an explicit `Name: "main"` in main.go for
161+
exactly this addressing), and every page-directed bridge call is
162+
window-scoped via `withWindow` — the bridge's `window` parameter
163+
defaults to "focused or first window", which with three windows
164+
was the root of every flip-flopping check and the missed badge.
165+
Second root cause, harness-repaired and upstream-worthy: the
166+
bridge's `call_bound_method` tool imports a SECOND runtime
167+
instance into the page (`await import('/wails/runtime.js')` in
168+
its own implementation), which re-registers
169+
`window._wails.dispatchWailsEvent` and permanently orphans the
170+
app bundle's event listeners — all live-sync (and the footer
171+
clock) dies at the first bound call. Every bound call in the
172+
harness therefore goes through `callBoundJSON`, which re-chains
173+
the captured app dispatcher (`captureAppDispatch` /
174+
`repairAppDispatch`, checks.go). This defect is bridge-client
175+
triggered only — a desktop build nobody drives over MCP never
176+
imports the second instance — but any future agent-drives-the-
177+
desktop-app work must reuse the same repair. CI job: stays wired,
178+
non-required, promote after a green track record on the
179+
calibrated registry.
168180
- **Manual-only registry** — OS-bound checks (hotkey delivery, real
169181
clipboard, tray) listed explicitly with reasons, never silently
170182
absent (see goal 0010's enforcement). Non-seed instance: the

.ls-lint.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@
5252
# added here -- that's the point, not a bug: root additions are
5353
# layout decisions, never accidents.
5454
ls:
55-
.go: regex:(main|singleinstance_(production|dev))
55+
# auxwindows: the ADR-0033 second-window constructors (Quick Panel +
56+
# approval prompt), split from main.go along that family seam when it
57+
# crossed the 500-line convention -- `package main` (they wire into
58+
# main.go's app object) so they cannot move under internal/.
59+
.go: regex:(main|auxwindows|singleinstance_(production|dev))
5660
# SECURITY/CONTRIBUTING added goal 0028 (public-repo hygiene): both are
5761
# standard GitHub community-profile root files, same allowlist family as
5862
# README below (checked, not new -- CODE_OF_CONDUCT deliberately stays

auxwindows.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import "github.com/wailsapp/wails/v3/pkg/application"
4+
5+
// The ADR-0033 second-window family: always-alive auxiliary windows
6+
// created once at startup, Hidden, shown/hidden for the app's life
7+
// (never destroyed/recreated). Split from main.go along that family
8+
// seam (the 500-line convention); main.go keeps the main window, tray,
9+
// and service wiring.
10+
11+
// newQuickPanelWindow builds the Quick Panel (docs/adr/0033): a
12+
// floating window the summon hotkey toggles. URL is a hash route, not
13+
// a bare path: production asset serving has no SPA fallback, so a bare
14+
// path second window would 404 in a real installed build. Deliberately
15+
// NOT ActivationPolicyAccessory (would pull Mill's dock icon too) and
16+
// NOT a non-activating NSPanel (unmerged upstream at beta.4) --
17+
// showing this window still activates Mill and steals focus, which
18+
// SettingsService's yieldFocusIfMainHidden mitigates on dismiss.
19+
func newQuickPanelWindow(app *application.App) *application.WebviewWindow {
20+
return app.Window.NewWithOptions(application.WebviewWindowOptions{
21+
Name: "quickpanel",
22+
Title: "Mill Quick Panel",
23+
Width: 560,
24+
Height: 400,
25+
Hidden: true,
26+
Frameless: true,
27+
DisableResize: true,
28+
InitialPosition: application.WindowCentered,
29+
HideOnFocusLost: true,
30+
HideOnEscape: true,
31+
BackgroundColour: application.NewRGB(6, 7, 15),
32+
Mac: application.MacWindow{
33+
Backdrop: application.MacBackdropTranslucent,
34+
WindowLevel: application.MacWindowLevelFloating,
35+
CollectionBehavior: application.MacWindowCollectionBehaviorCanJoinAllSpaces | application.MacWindowCollectionBehaviorFullScreenAuxiliary,
36+
TitleBar: application.MacTitleBar{
37+
AppearsTransparent: true,
38+
Hide: true,
39+
},
40+
},
41+
URL: "/#/quickpanel",
42+
})
43+
}
44+
45+
// newApprovalPromptWindow builds the floating approval prompt
46+
// (docs/goals/0023-attention-escalation.md item 1): the incoming-call/
47+
// askpass pattern, ADR-0033's second-window mechanism reused rather
48+
// than re-derived. Shown by the BACKEND itself
49+
// (SettingsService.NotifyPendingApproval's away verdict), never by a
50+
// hotkey. Deliberately NOT HideOnFocusLost (unlike the Quick Panel):
51+
// a decision prompt must not vanish just because focus wandered --
52+
// Escape (HideOnEscape) is its one explicit, native dismiss path.
53+
func newApprovalPromptWindow(app *application.App) *application.WebviewWindow {
54+
return app.Window.NewWithOptions(application.WebviewWindowOptions{
55+
Name: "approvalprompt",
56+
Title: "Mill Approval",
57+
Width: 520,
58+
Height: 200,
59+
Hidden: true,
60+
Frameless: true,
61+
DisableResize: true,
62+
InitialPosition: application.WindowCentered,
63+
HideOnEscape: true,
64+
BackgroundColour: application.NewRGB(6, 7, 15),
65+
Mac: application.MacWindow{
66+
Backdrop: application.MacBackdropTranslucent,
67+
WindowLevel: application.MacWindowLevelFloating,
68+
CollectionBehavior: application.MacWindowCollectionBehaviorCanJoinAllSpaces | application.MacWindowCollectionBehaviorFullScreenAuxiliary,
69+
TitleBar: application.MacTitleBar{
70+
AppearsTransparent: true,
71+
Hide: true,
72+
},
73+
},
74+
URL: "/#/approvalprompt",
75+
})
76+
}

frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/settingsservice.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@ export function ResolveMCPWrite(id: string, approve: boolean): $CancellablePromi
324324
return $Call.ByID(2304068895, id, approve);
325325
}
326326

327+
/**
328+
* ResolveUpdateChannel returns the effective channel for this run: the
329+
* persisted preference when set, else the build's ldflags stamp.
330+
* main.go calls this once before SetUpdateChannel/InitUpdater so the
331+
* guard, the UI label, and the provider's feed all agree.
332+
*/
333+
export function ResolveUpdateChannel(buildChannel: string): $CancellablePromise<string> {
334+
return $Call.ByID(1332358316, buildChannel);
335+
}
336+
327337
/**
328338
* ResolvedMCPWrites lists every already-resolved MCP write still in its
329339
* 24h retention window (docs/goals/0026 item 6) -- Review's
@@ -460,6 +470,17 @@ export function SetPendingBadge(count: number): $CancellablePromise<void> {
460470
return $Call.ByID(4240483754, count);
461471
}
462472

473+
/**
474+
* SetUpdateChannelPreference persists the channel override. The
475+
* preference resolves at BOOT (ResolveUpdateChannel below): the update
476+
* provider's feed selection is fixed at Init, so a change applies
477+
* after the next restart -- the UI says so rather than pretending a
478+
* live switch happened.
479+
*/
480+
export function SetUpdateChannelPreference(pref: string): $CancellablePromise<void> {
481+
return $Call.ByID(218736037, pref);
482+
}
483+
463484
/**
464485
* SetWorkflowMinutesSaved overrides workflowID's estimate. minutes must
465486
* be positive -- Home's own formula (RunCount × MinutesPerRun) always
@@ -568,3 +589,11 @@ export function UnassignSummonHotkey(): $CancellablePromise<void> {
568589
export function UpdateChannel(): $CancellablePromise<string> {
569590
return $Call.ByID(1676930456);
570591
}
592+
593+
/**
594+
* UpdateChannelPreference returns the persisted channel override:
595+
* "" (follow the build's own channel), "beta", or "release".
596+
*/
597+
export function UpdateChannelPreference(): $CancellablePromise<string> {
598+
return $Call.ByID(2624864437);
599+
}

frontend/e2e/atlas-folder-import.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ test('add from folder: scan, partial accept, containment, and mirror rendering a
8989
await expect(overlay.getByTestId('atlas-page-mirror-path')).toHaveValue(/Reports\/Q1 Summary\.md$/)
9090
await expect(overlay.getByTestId('atlas-mirror-markdown')).toContainText('Numbers looked good across the board.')
9191

92+
// Mermaid fences render as inline SVG diagrams (goal 0108); a fence
93+
// that fails to parse keeps its original code block -- the fixture
94+
// carries one of each, so exactly one diagram and one surviving
95+
// fence prove both the render and the honest fallback.
96+
const diagram = overlay.getByTestId('atlas-mermaid-diagram')
97+
await expect(diagram).toHaveCount(1)
98+
await expect(diagram.locator('svg')).toBeVisible()
99+
await expect(overlay.getByTestId('atlas-mirror-markdown').locator('code.language-mermaid')).toHaveCount(1)
100+
92101
// Cleanup (testing.md's within-file/within-worker discipline): the
93102
// child card must go before its own container can be deleted. Once
94103
// "Reports" holds no children, it renders as a plain note card,

frontend/e2e/fixtures/server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export const UPDATES_RELEASE_SERVER_BASE_PORT = 9790
7070
export const UPDATES_RELEASE_MCP_BASE_PORT = 9810
7171
export const UPDATES_BETA_SERVER_BASE_PORT = 9815
7272
export const UPDATES_BETA_MCP_BASE_PORT = 9825
73+
// The channel-preference opt-in test: persists a store value and
74+
// reloads, so it needs its own server like the other updates cases.
75+
export const UPDATES_CHANNEL_PREF_SERVER_BASE_PORT = 10360
76+
export const UPDATES_CHANNEL_PREF_MCP_BASE_PORT = 10380
7377
// guardrail-authoring.spec.ts's own dedicated pair (goal 0078): the
7478
// full rule-from-park -> unstick -> audit-edit -> policy-removed loop
7579
// asserts exact rule counts/groupings in the Rules audit view, which
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
11
# Q1 summary
22

33
Numbers looked good across the board.
4+
5+
```mermaid
6+
graph TD;
7+
Pilot-->Rollout;
8+
```
9+
10+
```mermaid
11+
this is not a diagram %% deliberately invalid
12+
```

frontend/e2e/updates.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
type SpawnedServer,
88
UPDATES_BETA_MCP_BASE_PORT,
99
UPDATES_BETA_SERVER_BASE_PORT,
10+
UPDATES_CHANNEL_PREF_MCP_BASE_PORT,
11+
UPDATES_CHANNEL_PREF_SERVER_BASE_PORT,
1012
UPDATES_RELEASE_MCP_BASE_PORT,
1113
UPDATES_RELEASE_SERVER_BASE_PORT,
1214
UPDATES_SOURCE_MCP_BASE_PORT,
@@ -150,3 +152,38 @@ test('Beta-channel build shows the primary Update now button and the beta channe
150152
await browser.close()
151153
}
152154
})
155+
156+
// The channel opt-in (goal 0100 follow-up slice): a
157+
// source-built copy can point the updater at the beta feed. The
158+
// preference persists to the settings store and applies on the next
159+
// boot (the provider's feed is fixed at Init), so the UI must say so
160+
// -- this pins the select, the saved-note, and persistence across a
161+
// reload.
162+
// eslint-disable-next-line no-empty-pattern -- this test needs `testInfo` (the second arg), not any fixture.
163+
test('Update-channel preference saves, explains the restart, and survives a reload', async ({}, testInfo) => {
164+
const idx = testInfo.parallelIndex
165+
let server: SpawnedServer | undefined
166+
let dir: string | undefined
167+
const browser = await chromium.launch()
168+
try {
169+
;({ server, dir } = await spawnUpdatesServer(idx, UPDATES_CHANNEL_PREF_SERVER_BASE_PORT, UPDATES_CHANNEL_PREF_MCP_BASE_PORT, {}))
170+
const page = await browser.newPage()
171+
await page.goto(`${server.baseURL}/`)
172+
await page.getByRole('link', { name: 'Settings' }).click()
173+
174+
const select = page.getByTestId('update-channel-select')
175+
await expect(select).toHaveValue('')
176+
await select.selectOption('beta')
177+
await expect(page.getByTestId('update-channel-saved')).toContainText('Restart Mill')
178+
179+
await page.reload()
180+
await page.getByRole('link', { name: 'Settings' }).click()
181+
await expect(page.getByTestId('update-channel-select')).toHaveValue('beta')
182+
183+
await page.close()
184+
} finally {
185+
await server?.stop()
186+
if (dir) rmSync(dir, { recursive: true, force: true })
187+
await browser.close()
188+
}
189+
})

0 commit comments

Comments
 (0)