Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0fa0375
fix(GenesisKit, GenesisHub, widgets): the 88 UX gaps from the 2026-10…
genesiscz Oct 24, 2026
58e9a07
test(GenesisKit): the widget, recording and daemon tests read the new…
genesiscz Oct 24, 2026
5f9c739
test(GenesisKit): the rail badge, alert title, recorder failure and O…
genesiscz Oct 24, 2026
f6bb987
fix(GenesisKit, GenesisHub, widgets): review round on the UX fixes: u…
genesiscz Oct 25, 2026
e6247b1
fix(GenesisKit, permissions): review round on the UX fixes: a granted…
genesiscz Oct 25, 2026
35bf21e
fix(GenesisHub, settings): review round on the UX fixes: one hub noti…
genesiscz Oct 25, 2026
4bfd05b
fix(GenesisKit, permissions): bot review on the UX fixes: the approva…
genesiscz Oct 25, 2026
f650786
fix(GenesisKit, GenesisHub, widgets): bot review on the UX fixes: a d…
genesiscz Oct 25, 2026
0634fe0
fix(GenesisHub, settings): bot review on the UX fixes: settings searc…
genesiscz Oct 25, 2026
581ad6b
fix(GenesisKit, GenesisHub, widgets): the staged list declares Reduce…
genesiscz Oct 25, 2026
af46590
fix(GenesisKit, permissions): the permissions window rechecks only th…
genesiscz Oct 25, 2026
187828e
fix(GenesisKit, GenesisHub, widgets): bot review round 2: with Send a…
genesiscz Oct 25, 2026
bfb77cb
fix(GenesisHub): bot review round 2: a pane notice carries its tone, …
genesiscz Oct 25, 2026
5a7750a
fix(GenesisKit, GenesisHub, widgets): bot review round 3: settings se…
genesiscz Oct 25, 2026
04ea28b
fix(GenesisKit, GenesisHub, widgets): #563 followup - a decision dism…
genesiscz Oct 26, 2026
545cbe8
fix(GenesisKit, GenesisHub, settings): #563 followup - every settings…
genesiscz Oct 26, 2026
043e0de
fix(GenesisKit, GenesisHub, widgets): #563 followup - a list read tha…
genesiscz Oct 26, 2026
87970e6
fix(GenesisKit, GenesisHub, widgets): #563 followup - the removal and…
genesiscz Oct 26, 2026
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
6 changes: 4 additions & 2 deletions src/hub/lib/composer/outbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2270,7 +2270,7 @@ describe("durable capture and file shelf", () => {
const { item } = await importShelfFile({ root: directory, input });
await unlink(item.path);
await expect(attachShelfItem({ root: directory, id: item.id, key: widgetSessionKey(target) })).rejects.toThrow(
"unavailable"
"no longer on the shelf"
);
expect((await readWidgetState(directory)).drafts).toEqual({});
});
Expand Down Expand Up @@ -2665,7 +2665,9 @@ test("shelf attachment descriptors inspect metadata and draft without creating o
);
expect(await stateFile.bytes()).toEqual(before);
const absent = join(directory, "not-created");
await expect(readShelfAttachment({ root: absent, id: "missing", key: "chosen" })).rejects.toThrow("unavailable");
await expect(readShelfAttachment({ root: absent, id: "missing", key: "chosen" })).rejects.toThrow(
"no longer on the shelf"
);
expect(await Bun.file(join(absent, "state.json")).exists()).toBe(false);
});

Expand Down
4 changes: 2 additions & 2 deletions src/hub/lib/widget/shelf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ export async function readShelfAttachment({ root, id, key }: { root?: string; id

const item = (await listWidgetShelf(root)).items.find((entry) => entry.id === id);
if (!item || !(await Bun.file(item.path).exists())) {
throw new Error("The staged item is unavailable; import it again");
throw new Error("This item is no longer on the shelf. Add it again.");
}

const state = await readWidgetState(root);
Expand Down Expand Up @@ -390,7 +390,7 @@ export async function attachShelfItem({

const item = (await listWidgetShelf(root)).items.find((entry) => entry.id === id);
if (!item || !(await Bun.file(item.path).exists())) {
throw new Error("The staged item is unavailable; import it again");
throw new Error("This item is no longer on the shelf. Add it again.");
}

return mutateWidgetState(root, (state) => {
Expand Down
13 changes: 13 additions & 0 deletions src/hub/lib/widget/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ export const widgetPreferencesSchema = z.object({
overviewFilter: z.enum(["all", "needs", "unread"]).default("all"),
/** The composer's microphone: records a voice note and puts its transcript in the field ("note"), or live dictation. */
composerVoice: z.enum(["ask", "note", "live"]).default("ask"),
/** Seconds the alert popup stays while the pointer is not on it. */
alertPopupSeconds: z.number().int().min(3).max(60).default(7),
/** Seconds a widget note stays before it fades; 0 keeps it until it is closed. A note with a button stays 10 s or more. */
noteSeconds: z.number().int().min(0).max(60).default(6),
/** A global shortcut opens and closes the widget from any app. */
openShortcutEnabled: z.boolean().default(true),
/** That shortcut: a Carbon virtual key code and modifier mask (GenesisKit `HotkeyChord`); ⌃⌥⌘W by default. */
openShortcut: z
.object({
keyCode: z.number().int().min(0).max(255),
modifiers: z.number().int().min(0).max(0xffff),
})
.default({ keyCode: 0x0d, modifiers: 0x1900 }),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

// Zod defaults also run inside partial objects; a patch must retain only supplied keys.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public enum VoiceMicrophonePermission: Equatable, Sendable {
switch self {
case .authorized: return nil
case .notDetermined: return "Microphone access is needed. Press Record to review the macOS permission prompt."
case .denied: return "Microphone access is off. Enable this app in System Settings → Privacy & Security → Microphone."
case .denied:
let app = PermissionKind.appName
return "Microphone access is off. In System Settings, open Privacy & Security, then Microphone and turn on \(app)."
case .restricted: return "Microphone access is restricted by this Mac's settings or administrator."
case .unknown: return "Microphone permission could not be checked. Review Microphone settings before recording."
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,28 @@ public struct RecordingControlsView: View {
.modifier(NativeGlassShapeSurface(shape: RoundedRectangle(cornerRadius: 14, style: .continuous)))
.environment(\.colorScheme, .dark)
.animation(reduceMotion ? nil : .easeOut(duration: 0.16), value: confirming)
.onChange(of: state.phase) { _, phase in
.onChange(of: state.phase) { old, phase in
if !phase.isActive || phase == .finishing { confirming = nil }
announce(from: old, to: phase)
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Recording controls")
}

/// Phases change by themselves (the recorder starts late, the limit pauses), so VoiceOver is told.
private func announce(from old: RecordingPhase, to phase: RecordingPhase) {
switch phase {
case .recording:
GenAnnounce.post(old == .paused ? "Recording resumed" : "Recording started")
case .paused:
GenAnnounce.post(state.pauseReason == "limit" ? "Recording paused, the length limit is reached" : "Recording paused")
case .finishing:
GenAnnounce.post("Recording stopped, saving")
default:
break
}
}

// MARK: Status (the drag area)

private var status: some View {
Expand All @@ -177,7 +192,7 @@ public struct RecordingControlsView: View {
.font(.system(size: 13, weight: .semibold).monospacedDigit())
.foregroundStyle(paused ? .secondary : .primary)
if let caption = statusCaption {
Text(caption).font(.system(size: 9.5, weight: .medium))
Text(caption).font(.system(size: 11, weight: .medium))
.foregroundStyle(captionColor)
.lineLimit(1)
}
Expand Down Expand Up @@ -223,7 +238,7 @@ public struct RecordingControlsView: View {
}

private var captionColor: Color {
state.phase == .paused || remainingMs != nil ? .orange : .secondary
state.phase == .paused || remainingMs != nil ? .orange : SessionPalette.secondary
}

private var accessibilityStatus: String {
Expand Down Expand Up @@ -285,7 +300,7 @@ public struct RecordingControlsView: View {
.frameProbe("recording.pause")
}
}
IconButton(systemName: "arrow.counterclockwise", tooltip: "Restart: drop what was recorded and start again",
IconButton(systemName: "arrow.counterclockwise", tooltip: "Restart: discard what was recorded and start again",
size: 13) {
ask(.restart, then: actions.restart)
}
Expand All @@ -310,7 +325,7 @@ public struct RecordingControlsView: View {
.accessibilityLabel(state.useTitle)
Comment thread
genesiscz marked this conversation as resolved.
.accessibilityIdentifier("recording.use")
.frameProbe("recording.use")
IconButton(systemName: "trash", tooltip: "Stop and discard", size: 13) {
IconButton(systemName: "trash", tooltip: "Discard the recording", size: 13) {
ask(.discard, then: actions.discard)
}
.disabled(busy)
Expand All @@ -334,7 +349,7 @@ public struct RecordingControlsView: View {
HStack(spacing: 8) {
Text(kind == .discard
? "Discard \(RecordingClock.format(state.elapsedMs)) of recording?"
: "Restart and drop \(RecordingClock.format(state.elapsedMs))?")
: "Restart and discard \(RecordingClock.format(state.elapsedMs))?")
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
.fixedSize()
Expand Down Expand Up @@ -370,7 +385,7 @@ public struct RecordingControlsView: View {
private func preparingRow(_ text: String) -> some View {
HStack(alignment: .top, spacing: 6) {
Image(systemName: "hourglass").font(.system(size: 10)).foregroundStyle(.secondary)
Text(text).font(.system(size: 11)).foregroundStyle(.secondary)
Text(text).font(.system(size: 11)).foregroundStyle(SessionPalette.secondary)
.lineLimit(3).fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: 420, alignment: .leading)
Spacer(minLength: 0)
Expand All @@ -383,7 +398,7 @@ public struct RecordingControlsView: View {
private func noticeRow(_ notice: String) -> some View {
HStack(alignment: .top, spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill").font(.system(size: 10)).foregroundStyle(.orange)
Text(notice).font(.system(size: 11)).foregroundStyle(.secondary)
Text(notice).font(.system(size: 11)).foregroundStyle(SessionPalette.secondary)
.lineLimit(3).fixedSize(horizontal: false, vertical: true)
.textSelection(.enabled)
.frame(maxWidth: 420, alignment: .leading)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ public final class RecordingSessionController: ObservableObject {
guard !ended else { return }
ended = true
phase = .failed
// One sentence on screen; the recorder's own words go to the log.
let tail = stderr.trimmingCharacters(in: .whitespacesAndNewlines).suffix(600)
onFailed?(tail.isEmpty ? "The recorder stopped unexpectedly (exit \(status))." : String(tail), nil)
KitHost.log("recording.session stderr (exit \(status)) \(tail.isEmpty ? "(empty)" : String(tail))")
onFailed?("The recorder stopped unexpectedly. The details are in the log.", nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public final class RecordingSettings: ObservableObject {
didSet { defaults.set(showsBorder, forKey: Key.showsBorder) }
}

/// "Stop and discard" and "Restart" ask once inside the controls before they drop what was recorded. Default on.
/// "Discard" and "Restart" ask once inside the controls before they lose what was recorded. Default on.
@Published public var confirmsDiscard: Bool {
didSet { defaults.set(confirmsDiscard, forKey: Key.confirmsDiscard) }
}
Expand Down Expand Up @@ -93,7 +93,7 @@ public struct RecordingSettingsView: View {
}
Divider()
NativeSettingsToggle(
"Ask before discarding", detail: "Stop and discard, and Restart, ask once inside the controls.",
"Ask before discarding", detail: "Discard and Restart ask once inside the controls.",
identifier: "recording.confirmsDiscard", isOn: $settings.confirmsDiscard)
}
NativeSettingsCard("Recording") {
Expand Down
Loading
Loading