Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Settings
created: 2026-10-19T20:51:03Z
norm: '1'
sig: 8d6f8142ef1a23f2
body_hash: ab1332a896bdd893
raw_hash: b048e59ec08c6130
vouched: 2026-10-19T21:09:57Z
body_hash: 96040bd24f7ca57e
raw_hash: 8e414b5bdc84fe31
vouched: 2026-10-22T15:02:42Z
lines: 325-537
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Settings
created: 2026-10-19T20:53:03Z
norm: '1'
sig: 8d6f8142ef1a23f2
body_hash: ab1332a896bdd893
raw_hash: b048e59ec08c6130
vouched: 2026-10-19T21:09:57Z
body_hash: 96040bd24f7ca57e
raw_hash: 8e414b5bdc84fe31
vouched: 2026-10-22T15:02:42Z
lines: 325-545
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-15T03:39:45Z
norm: '1'
sig: 171797818ef4d836
body_hash: 50b83460ae5ee675
raw_hash: f38372c6ca8389a3
vouched: 2026-10-22T14:33:27Z
body_hash: 1332a8dc2a8a58e7
raw_hash: 99138b0aa8226285
vouched: 2026-10-22T15:02:43Z
lines: 668-751
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-15T10:17:10Z
norm: '1'
sig: 171797818ef4d836
body_hash: 50b83460ae5ee675
raw_hash: f38372c6ca8389a3
vouched: 2026-10-22T14:33:27Z
body_hash: 1332a8dc2a8a58e7
raw_hash: 99138b0aa8226285
vouched: 2026-10-22T15:02:43Z
lines: 830-948
supersedes: 01M4Z2R1HEVDKEA8VEJ7XRJQ8F
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-20T17:04:56Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: 0fab689dd7631f93
raw_hash: b556858d2f701dda
vouched: 2026-10-22T14:00:57Z
body_hash: fa085b53cc7365ec
raw_hash: 40117df15a7c5ea7
vouched: 2026-10-22T15:02:43Z
lines: 1-1153
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: '@file'
created: 2026-10-22T14:00:47Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: 0fab689dd7631f93
raw_hash: b556858d2f701dda
body_hash: fa085b53cc7365ec
raw_hash: 40117df15a7c5ea7
vouched: 2026-10-22T15:02:43Z
lines: 1-1309
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function useComposer
created: 2026-10-16T22:41:13Z
norm: '1'
sig: 381929b0cbaf2245
body_hash: 4dcfff341604590f
raw_hash: 03d5450461fb3892
vouched: 2026-10-22T14:39:32Z
body_hash: 17515b9210059870
raw_hash: 30786624d9379566
vouched: 2026-10-22T15:02:44Z
lines: 597-1226
---

Expand Down
3 changes: 2 additions & 1 deletion apps/app/src/channel-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ function Machine({ channel }: { channel: Listing }) {
}
>
<Icon className="size-3.5 shrink-0" aria-hidden />
<span className="max-w-40 truncate">{name}</span>
{/* A phone's tab strip needs the room; the icon's tooltip still names the machine. */}
<span className="max-w-40 truncate max-sm:sr-only">{name}</span>
</TooltipTrigger>
<TooltipContent>{detail}</TooltipContent>
</Tooltip>
Expand Down
14 changes: 10 additions & 4 deletions apps/app/src/host-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export function HostStatus({ active }: { active: boolean }) {
)}
{checked?.tailscale.state === "running" && !checked.tailscale.loopback && (
<Group title="Phone">
<Phone web={checked.web} />
<Phone web={checked.web} http={checked.http} />
</Group>
)}
<Group
Expand Down Expand Up @@ -250,9 +250,13 @@ export function HostStatus({ active }: { active: boolean }) {
);
}

/** This host's HTTPS address, which the owner's phone opens over the tailnet. */
function Phone({ web }: { web?: string }) {
/**
* The address the owner's phone opens over the tailnet: HTTPS when the tailnet has certificates,
* else plain HTTP, which works except for the microphone.
*/
function Phone({ web: secure, http }: { web?: string; http?: string }) {
const [copied, setCopied] = useState(false);
const web = secure || http;
if (!web) {
return (
<Row
Expand All @@ -264,7 +268,9 @@ function Phone({ web }: { web?: string }) {
return (
<Row
label="Open on your phone"
description="Scan with a phone on your tailnet."
description={secure
? "Scan with a phone on your tailnet."
: "Scan with a phone on your tailnet. Browsers keep the microphone off over HTTP; for voice input, turn on HTTPS in the Tailscale admin console's DNS settings, then restart Ace Helper."}
footer={
<div className="flex flex-wrap items-center gap-4">
<Qr value={web} label={`QR code for ${web}`} className="size-36 rounded-md" />
Expand Down
9 changes: 6 additions & 3 deletions apps/app/src/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,16 +425,19 @@ export function Settings(
<DialogTitle className="px-2 pt-1">Settings</DialogTitle>
<nav
aria-label="Settings sections"
className="flex gap-0.5 overflow-x-auto scrollbar-hidden sm:flex-col sm:gap-4"
className="flex flex-wrap gap-0.5 sm:flex-col sm:flex-nowrap sm:gap-4"
>
{groups.map((group) => (
<div key={group.label || "app"} className="flex shrink-0 gap-0.5 sm:flex-col">
<div
key={group.label || "app"}
className="contents sm:flex sm:shrink-0 sm:flex-col sm:gap-0.5"
>
{group.label && (
<h3 className="hidden px-2 pb-1 text-xs text-muted-foreground sm:block">
{group.label}
</h3>
)}
<ul className="flex gap-0.5 sm:flex-col">
<ul className="contents sm:flex sm:flex-col sm:gap-0.5">
{group.sections.map(({ id, label, icon: Icon }) => (
<li key={id} className="shrink-0">
<button
Expand Down
1 change: 1 addition & 0 deletions apps/host/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,6 @@ export async function diagnostics(sync?: boolean): Promise<Diagnostics> {
},
shell: { ...environment },
web: web.address,
http: web.plain,
};
}
1 change: 1 addition & 0 deletions apps/host/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,7 @@ export async function serve(port: number, loopback = false, script?: string): Pr
}
if (machine) {
servers.push(team(machine, { port, secure: false }));
web.served(machine, { port, secure: false });
const address = isIP(machine.address) === 6 ? `[${machine.address}]` : machine.address;
servedUrl = `http://${address}:${port}/directory`;
console.log(`Sharing with the tailnet on ${machine.address}:${port} as ${machine.login}`);
Expand Down
2 changes: 2 additions & 0 deletions apps/host/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export type Diagnostics = {
shell: { state: "adopted" | "pending" | "failed"; error?: string };
/** This host's HTTPS address on the tailnet; unset without a tailnet certificate. */
web?: string;
/** This host's plain HTTP address on the tailnet; unset off the tailnet. */
http?: string;
};

/** `offline`: the channel's host is unreachable. Hosted channels are never offline. */
Expand Down
6 changes: 5 additions & 1 deletion apps/host/src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@ export type Listener = { port: number; secure: boolean };
/** The HTTPS address the owner's phone opens, once that listener is up. */
export let address: string | undefined;

/** The tailnet's plain HTTP address, which WireGuard encrypts but browsers treat as insecure. */
export let plain: string | undefined;

export function served(machine: Machine, listener: Listener): void {
address = `https://${machine.dns}:${listener.port}`;
if (listener.secure) address = `https://${machine.dns}:${listener.port}`;
else plain = `http://${machine.dns}:${listener.port}`;
}

/** Names this host answers to on the tailnet; anything else is DNS rebinding. */
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/components/app-layout/nav-list-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ function NavListLink(
<span
className={cn(
"relative z-1 text-xs font-medium pr-2 sm:opacity-0 sm:@min-[5rem]/nav:opacity-100 sm:text-sm",
// A phone's tab bar fits every icon and the account button by naming only the current page.
"max-[26rem]:sr-only max-[26rem]:group-data-[status=active]:not-sr-only",
"transition-opacity duration-200 ease-in-out",
)}
>
Expand Down
11 changes: 10 additions & 1 deletion packages/ui/src/components/composer/chat-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as fuzzysort from "fuzzysort";
import type { MarkType, Node as ProseMirrorNode, NodeType } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import { toast } from "sonner";

import { canDictate, useDictation } from "../../hooks/use-dictation";
import { useDraft } from "../../hooks/use-draft";
Expand Down Expand Up @@ -973,6 +974,14 @@ function useComposer({
}

function record() {
// Browsers expose the microphone only to secure pages, such as a host's tailnet HTTPS address.
if (!window.isSecureContext) {
toast("Voice input needs HTTPS", {
description:
"Browsers keep the microphone off over HTTP. Open Ace at this host's https:// address to dictate.",
});
return;
}
let { doc, selection: { from, to } } = view.current!.state;
// Like system dictation: replace the selection, and keep spaces between the words already
// there and the dictated ones.
Expand Down Expand Up @@ -1515,7 +1524,7 @@ function ComposerModel({ state }: { state: ComposerState }) {
<Button
variant="ghost"
size="sm"
className="-ml-0.5 h-6.5 px-2 text-muted-foreground"
className="-ml-0.5 h-6.5 min-w-0 shrink px-2 text-muted-foreground"
/>
}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/composer/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ function Body({ className, ref, onFiles, children, ...props }: BodyProps) {
<div
ref={ref}
className={cn(
"composer:body relative z-10 overflow-clip rounded-xl squircle bg-background shadow-composer [grid-area:composer]",
"composer:body relative z-10 min-w-0 overflow-clip rounded-xl squircle bg-background shadow-composer [grid-area:composer]",
className,
)}
data-dragging={dragging || undefined}
Expand Down