Skip to content

Commit c758d49

Browse files
committed
fix: complete day-one host release experience
1 parent 6a6afcf commit c758d49

24 files changed

Lines changed: 440 additions & 106 deletions

CHANGELOG.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.0.8] - 2026-07-24
11+
12+
### Fixed
13+
14+
- Successfully scheduling a durable agent follow-up now ends the active turn
15+
at the tool boundary. The runtime no longer makes a second model request or
16+
briefly exposes a fabricated completion before removing it, eliminating a
17+
timing race observed during the clean macOS release test.
18+
- Gmail OAuth can now be completed from a remote browser by pasting its final
19+
localhost callback URL into 1Helm. Automatic host-local callbacks still
20+
work; the fallback validates the same one-time state and PKCE exchange.
21+
22+
### Changed
23+
24+
- The app's feedback surface and project privacy, support, and security
25+
guidance now identify `build@1helm.com` as the company contact address.
26+
- Notification audio is now a per-user preference: every member can mute all
27+
pings globally, mute an individual channel, and choose that channel's sound
28+
without changing anyone else's experience.
29+
1030
## [0.0.7] - 2026-07-24
1131

1232
### Fixed
@@ -201,7 +221,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
201221
notarization, stapled tickets, Gatekeeper verification, persistent
202222
Application Support, and isolated Apple container machines.
203223

204-
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.7...HEAD
224+
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.8...HEAD
225+
[0.0.8]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.8
205226
[0.0.7]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.7
206227
[0.0.6]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.6
207228
[0.0.5]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.5

SECURITY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Use GitHub's **Report a vulnerability** flow for
1212
Include the affected version, platform, impact, minimum reproduction, and any
1313
relevant logs after removing credentials and personal data.
1414

15+
For non-sensitive security questions or company contact, email
16+
[`build@1helm.com`](mailto:build@1helm.com). Do not send unpatched vulnerability
17+
details or secrets by ordinary email; use the private advisory flow above.
18+
1519
Do not open a public issue for an unpatched vulnerability. Do not include API
1620
keys, OAuth tokens, Photon project secrets, private messages, workspace data,
1721
Apple signing material, or other people's personal information in a report.

desktop/main.cjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ function openAuthWindow(url) {
164164
window.webContents.on("will-navigate", returnToApp);
165165
window.webContents.on("will-redirect", returnToApp);
166166
window.webContents.setWindowOpenHandler(({ url: nextUrl }) => {
167-
if (/^https?:/i.test(nextUrl)) void shell.openExternal(nextUrl);
167+
if (/^https?:/i.test(nextUrl) || /^mailto:build@1helm\.com$/i.test(nextUrl)) void shell.openExternal(nextUrl);
168168
return { action: "deny" };
169169
});
170170
window.on("closed", () => { if (authWindow === window) authWindow = null; });
@@ -191,7 +191,7 @@ function createWindow(showWhenReady = true) {
191191

192192
window.webContents.setWindowOpenHandler(({ url }) => {
193193
if (allowedAppUrl(url)) return { action: "allow" };
194-
if (/^https?:/i.test(url)) void shell.openExternal(url);
194+
if (/^https?:/i.test(url) || /^mailto:build@1helm\.com$/i.test(url)) void shell.openExternal(url);
195195
return { action: "deny" };
196196
});
197197
window.webContents.on("will-navigate", (event, url) => {

docs/USER_GUIDE.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,9 @@ Settings → Connections → Gmail owns the connection on the 1Helm host.
224224
1. Once per Google Cloud project, create a Desktop app OAuth client, enable the
225225
Gmail API, and choose the downloaded JSON in 1Helm.
226226
2. Choose **Connect Gmail**. 1Helm opens a state-protected PKCE authorization and
227-
receives the callback on loopback.
227+
receives the callback on loopback. If the browser is on another device and
228+
its `127.0.0.1` page cannot load, copy the complete callback URL from that
229+
address bar and paste it into the Gmail connection card.
228230
3. Return to 1Helm after Google confirms the account.
229231

230232
![Gmail and Photon are native host-brokered connections](assets/guide/connections.png)
@@ -237,8 +239,10 @@ accounts to that resident. Gmail supports account inventory, search, read, and
237239
draft creation. Sending remains disabled. OAuth tokens live under 1Helm's
238240
host-owned data and never enter chat, Activity evidence, or a resident computer.
239241

240-
The loopback callback belongs to the installed/local 1Helm host. A remotely
241-
viewed demo cannot authorize Gmail on a different machine.
242+
The pasted callback is never fetched. 1Helm validates its one-time state and
243+
PKCE verifier, then exchanges the authorization code from the host where the
244+
tokens belong. Automatic loopback completion still works when the browser is
245+
running on that host.
242246

243247
## Photon / iMessage
244248

@@ -283,6 +287,15 @@ Settings → Members and create an LLM-independent notice in the Captain's
283287
model policy, or Files world. Channel membership gates HTTP, files, terminals,
284288
messages, and WebSocket fan-out.
285289

290+
For privacy, support, or company questions, contact
291+
[`build@1helm.com`](mailto:build@1helm.com). Sensitive vulnerability reports
292+
belong in the repository's private GitHub security-advisory flow rather than
293+
ordinary email.
294+
295+
Notification sounds are personal. Settings → Notifications provides a global
296+
mute for the signed-in account. Each channel's Settings view can mute only that
297+
channel or select its ping sound; these choices do not affect other members.
298+
286299
## Updates, removal, and recovery
287300

288301
Signed Mac releases are unique patch versions. Profile → Check for updates

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
{
22
"name": "1helm",
33
"productName": "1Helm",
4-
"version": "0.0.7",
4+
"version": "0.0.8",
55
"private": true,
66
"type": "module",
77
"description": "1Helm is the self-hosted home for durable AI employees: one resident, one private computer, compounding memory and skills, and Skipper for every boundary.",
8+
"author": {
9+
"name": "Joseph Yaksich",
10+
"email": "build@1helm.com"
11+
},
812
"main": "desktop/main.cjs",
913
"scripts": {
1014
"assets:brand": "node scripts/generate-brand-assets.cjs",

public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
</head>
3535
<body class="h-screen w-screen overflow-hidden antialiased">
3636
<div id="app" class="h-full w-full"></div>
37-
<script type="module" src="/bundle.js?v=21954b038710"></script>
37+
<script type="module" src="/bundle.js?v=6e8f6fac29f7"></script>
3838
</body>
3939
</html>

scripts/run-test-suite.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const suites = [
4646
"test/channel-computers.mjs", "test/channel-computers-isolated-backends.mjs", "test/channel-computers-backend-migration.mjs",
4747
"test/cloudflare-worker.mjs", "test/connectors.mjs", "test/chatgpt-image.mjs", "test/autonomy-platform.mjs",
4848
"test/feedback.mjs", "test/feedback-browser.mjs", "test/gmail.mjs", "test/photon.mjs", "test/site.mjs",
49-
"test/terminal-reconnect-contract.mjs", "test/terminal-reconnect-browser.mjs", "test/web-research.mjs", "test/workflows.mjs"],
49+
"test/notifications.mjs", "test/terminal-reconnect-contract.mjs", "test/terminal-reconnect-browser.mjs", "test/web-research.mjs", "test/workflows.mjs"],
5050
];
5151

5252
let status = 0;

src/client/app.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { api, downloadAuthenticatedFile, openAuthenticatedFile, uploadFile, connectEvents, getToken, setToken, clearToken, workspacePhotoSrc, type User, type Channel, type Message, type Bot, type Computer, type Provider, type Workspace, type ModelPolicy, type AgentProgress, type AgentQuestions, type ThreadUsage, type RoutingModel } from "./api.ts";
2-
import { h, clear, add, md, color, initials, timeLabel, dayLabel, sameDay, beep, icon, helmMark, type ChannelLink } from "./dom.ts";
2+
import { h, clear, add, md, color, initials, timeLabel, dayLabel, sameDay, icon, helmMark, type ChannelLink } from "./dom.ts";
33
import { openSettings, finishOpenRouterOAuth, refreshOpenSkillsSettings } from "./settings.ts";
4+
import { hydrateNotificationPreferences, playNotification } from "./notifications.ts";
45
import { pushRoutingActivity } from "./routing.ts";
56
import { openOnboarding } from "./onboarding.ts";
67
import { defaultTerminalComputer, openTerminals, refitChannelTerminals, getTerminalChrome } from "./term.ts";
@@ -103,6 +104,7 @@ function persistCurrentChannelView(): void {
103104
async function loadUiState(): Promise<void> {
104105
try {
105106
const result = await api<{ state: Record<string, unknown> }>("/api/me/ui-state");
107+
hydrateNotificationPreferences(result.state.notification_preferences);
106108
const next: Record<number, ChannelUiView> = {};
107109
for (const [key, value] of Object.entries(result.state || {})) {
108110
const match = /^channel_view:(\d+)$/.exec(key);
@@ -385,7 +387,7 @@ function onEvent(e: any): void {
385387
markChannelRead(msg.channel_id);
386388
unreadBadgeCounted.add(msg.id);
387389
}
388-
if (e.type === "message" && !mine) beep(mentionsMe ? "mention" : "msg");
390+
if (e.type === "message" && !mine) playNotification(msg.channel_id, mentionsMe ? "mention" : "msg");
389391
// Stream ticks mutate one or two message rows. Rebuilding the whole thread
390392
// panel here used to destroy the focused composer every 75 ms while an
391393
// agent was working, which also reset selection and made scrolling jump.
@@ -396,11 +398,11 @@ function onEvent(e: any): void {
396398
if (!unreadBadgeCounted.has(msg.id)) {
397399
unreadBadgeCounted.add(msg.id);
398400
bumpChannelUnread(msg.channel_id);
399-
beep(mentionsMe ? "mention" : "msg");
401+
playNotification(msg.channel_id, mentionsMe ? "mention" : "msg");
400402
}
401403
} else if (e.type === "message" && !mine && !messageIsSettled(msg)) {
402404
// Working… started in another channel — amber dots come from agent_status; no badge yet.
403-
beep(mentionsMe ? "mention" : "msg");
405+
playNotification(msg.channel_id, mentionsMe ? "mention" : "msg");
404406
}
405407
} else if (e.type === "message_deleted") {
406408
applyMessageDeleted(e);
@@ -868,7 +870,13 @@ function openFeedback(): void {
868870
h("div", { class: "flex items-start justify-between gap-3" },
869871
h("div", {},
870872
h("h2", { class: "font-display text-xl text-fg" }, "Send feedback"),
871-
h("p", { class: "mt-1 text-sm leading-6 text-muted" }, "Tell us what feels broken or what would make 1Helm better.")),
873+
h("p", { class: "mt-1 text-sm leading-6 text-muted" }, "Tell us what feels broken or what would make 1Helm better."),
874+
h("p", { class: "mt-1 text-xs leading-5 text-muted" }, "You can also email ", h("a", {
875+
class: "text-accent hover:underline",
876+
href: "mailto:build@1helm.com",
877+
target: "_blank",
878+
rel: "noopener",
879+
}, "build@1helm.com"), ".")),
872880
h("button", { class: "grid h-8 w-8 place-items-center rounded text-muted hover:bg-hover", "aria-label": "Close feedback", onclick: close }, icon("x"))),
873881
comment,
874882
h("div", {},

src/client/channel.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { api, downloadAuthenticatedFile, openAuthenticatedFile, uploadFile, type ActivityItem, type AgentTemplate, type Channel, type ChannelFile, type GlobalThread, type MemoryItem, type Message, type ThreadState, type RoutingModel } from "./api.ts";
22
import { h, clear, icon, md, timeLabel } from "./dom.ts";
33
import { S, avatar, appAlert, appConfirm, appPrompt } from "./app.ts";
4+
import { NOTIFICATION_SOUNDS, channelNotificationPreference, previewNotification, setChannelNotificationPreference } from "./notifications.ts";
45

56
export type ChannelView = "chat" | "board" | "threads" | "files" | "terminal" | "memory" | "activity" | "settings";
67

@@ -614,6 +615,21 @@ export function renderChannelSettings(container: HTMLElement, channel: Channel,
614615
const provider = h("select", { class: "field" }, h("option", { value: "" }, "Loading providers…")) as HTMLSelectElement;
615616
const model = h("select", { class: "field" }, h("option", { value: channel.agent?.model || "" }, channel.agent?.model || "Choose a model")) as HTMLSelectElement;
616617
const status = h("p", { class: "min-h-5 text-sm text-muted" });
618+
const notificationPreference = channelNotificationPreference(channel.id);
619+
const channelMuted = h("input", { type: "checkbox", checked: notificationPreference.muted, class: "accent-accent" }) as HTMLInputElement;
620+
const channelSound = h("select", { class: "field" }, ...NOTIFICATION_SOUNDS.map((item) => h("option", { value: item.value, selected: item.value === notificationPreference.sound }, item.label))) as HTMLSelectElement;
621+
const notificationStatus = h("p", { class: "min-h-5 text-sm text-muted" });
622+
const saveChannelNotifications = async (): Promise<void> => {
623+
channelMuted.disabled = true; channelSound.disabled = true; notificationStatus.textContent = "Saving…";
624+
try {
625+
await setChannelNotificationPreference(channel.id, { muted: channelMuted.checked, sound: channelSound.value as typeof notificationPreference.sound });
626+
notificationStatus.textContent = channelMuted.checked ? `#${channel.name} is muted for your account.` : `#${channel.name} will use ${channelSound.selectedOptions[0]?.textContent || "this sound"}.`;
627+
} catch (error) { notificationStatus.textContent = (error as Error).message; }
628+
finally { channelMuted.disabled = false; channelSound.disabled = channelMuted.checked; }
629+
};
630+
channelMuted.onchange = () => { channelSound.disabled = channelMuted.checked; void saveChannelNotifications(); };
631+
channelSound.onchange = () => { previewNotification(channelSound.value as typeof notificationPreference.sound); void saveChannelNotifications(); };
632+
channelSound.disabled = channelMuted.checked;
617633
let loadSequence = 0;
618634
let modelLoading = false;
619635
let changeModelButton: HTMLButtonElement | null = null;
@@ -744,6 +760,11 @@ export function renderChannelSettings(container: HTMLElement, channel: Channel,
744760
? null
745761
: h("button", { class: "btn-primary text-sm", onclick: () => { void saveName(); } }, "Rename"))),
746762
h("div", { class: "card space-y-3 p-4" }, h("h3", { class: "font-semibold text-fg" }, "Purpose"), purpose, h("div", { class: "flex justify-end" }, h("button", { class: "btn-primary text-sm", onclick: () => { void savePurpose(); } }, "Save purpose"))),
763+
h("div", { class: "card space-y-3 p-4", dataset: { channelNotifications: "" } },
764+
h("div", {}, h("h3", { class: "font-semibold text-fg" }, "Notification sound"), h("p", { class: "mt-1 text-sm leading-6 text-muted" }, "Private to your account. Global mute in Settings → Notifications always takes priority.")),
765+
h("label", { class: "flex items-center gap-3 rounded-lg border border-line bg-panel p-3 text-sm font-semibold text-fg" }, channelMuted, `Mute #${channel.name}`),
766+
h("label", { class: "block space-y-1 text-xs font-semibold text-fg" }, "Ping sound", channelSound),
767+
notificationStatus),
747768
h("div", { class: "card space-y-3 p-4" },
748769
h("div", {}, h("h3", { class: "font-semibold text-fg" }, "Agent avatar"), h("p", { class: "mt-1 text-sm text-muted" }, "Pick a flat color or upload a custom image for this resident agent.")),
749770
agentAvatar, h("div", { class: "mt-2" }, h("span", { class: "mb-1 block text-xs font-semibold text-muted" }, "Default colors"), avatarColorRow)),

0 commit comments

Comments
 (0)