Skip to content

Commit c0b97be

Browse files
katipallyCopilot
andcommitted
Add in-app HelpTip tooltips and overhaul documentation
- New HelpTip.swift: reusable ⓘ hover-popover component (22 spots) - PanelRootView: tooltips on master toggle, mode selector, duration strip, elapsed badge - TrackAgentsPopover: tooltip on Pause toggle (explains in-memory reset) - ConfigureAgentsWindowV2: tooltips on health dot, hook warning badge, channel overrides, iCloud status, all 7 notification event pref toggles - ConfigureSettingsPane: tooltips on Screen Off re-arm, auto-revert timer, redact prompt text - README.md: full overhaul — adds session timer, Screen Off re-arm, pause flag, per-agent toggles, notification event prefs, hotkey, VS Code variants, auto-revert, redact, Connection Doctor, Logs, Live Events, migration, supported agents table with capabilities and config paths - docs/features.md: new comprehensive feature reference covering every setting, default value, hook pipeline, data storage, and technical details Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 65257cd commit c0b97be

8 files changed

Lines changed: 555 additions & 50 deletions

File tree

DoomCoder.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
1C76E5FECBDED303B300FB6A /* CloudKitPusherDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF9FDE1DC4D56B4433A07055 /* CloudKitPusherDelegate.swift */; };
1212
581BCAD520D6B5F4E8AE92B9 /* CloudKitPusherLifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3A8884C2E1245780700D4E /* CloudKitPusherLifecycle.swift */; };
1313
75DA2F57E2420F9EC7D0C65C /* DoomCoderCore in Frameworks */ = {isa = PBXBuildFile; productRef = 1E5CD75DFC788EE9A2743B1C /* DoomCoderCore */; };
14+
DC0000000000002000000123 /* HelpTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000123 /* HelpTip.swift */; };
1415
DC0000000000002000000002 /* DoomCoderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000002 /* DoomCoderApp.swift */; };
1516
DC0000000000002000000003 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000003 /* SleepManager.swift */; };
1617
DC0000000000002000000005 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000005 /* AboutView.swift */; };
@@ -63,6 +64,7 @@
6364
AA3A8884C2E1245780700D4E /* CloudKitPusherLifecycle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudKitPusherLifecycle.swift; sourceTree = "<group>"; };
6465
CF9FDE1DC4D56B4433A07055 /* CloudKitPusherDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudKitPusherDelegate.swift; sourceTree = "<group>"; };
6566
DC0000000000001000000001 /* DoomCoder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DoomCoder.app; sourceTree = BUILT_PRODUCTS_DIR; };
67+
DC0000000000001000000123 /* HelpTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpTip.swift; sourceTree = "<group>"; };
6668
DC0000000000001000000002 /* DoomCoderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoomCoderApp.swift; sourceTree = "<group>"; };
6769
DC0000000000001000000003 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
6870
DC0000000000001000000005 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
@@ -134,6 +136,7 @@
134136
DC0000000000003000000002 /* DoomCoder */ = {
135137
isa = PBXGroup;
136138
children = (
139+
DC0000000000001000000123 /* HelpTip.swift */,
137140
DC0000000000001000000002 /* DoomCoderApp.swift */,
138141
DC0000000000001000000003 /* SleepManager.swift */,
139142
DC0000000000001000000005 /* AboutView.swift */,
@@ -306,6 +309,7 @@
306309
isa = PBXSourcesBuildPhase;
307310
buildActionMask = 2147483647;
308311
files = (
312+
DC0000000000002000000123 /* HelpTip.swift in Sources */,
309313
DC0000000000002000000002 /* DoomCoderApp.swift in Sources */,
310314
DC0000000000002000000003 /* SleepManager.swift in Sources */,
311315
DC0000000000002000000005 /* AboutView.swift in Sources */,

DoomCoder/AgentTracking/ConfigureAgentsWindowV2.swift

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,13 @@ struct ConfigureAgentsViewV2: View {
175175
}
176176
Spacer()
177177
if hasWarning {
178+
HelpTip("DoomCoder detected that the installed hook config no longer matches what it wrote — likely edited by the agent or another tool. Select this agent and click Repair to restore it.")
178179
Image(systemName: "exclamationmark.triangle.fill")
179180
.foregroundStyle(.yellow)
180181
.font(.caption)
181182
} else if isInst {
182183
// Health dot: green if events in last hour, grey otherwise
184+
HelpTip("Green = at least one hook event received in the last hour. Grey = no recent activity (agent may be idle or not running).")
183185
Circle()
184186
.fill(eventCount > 0 ? Color.green : Color.secondary.opacity(0.3))
185187
.frame(width: 8, height: 8)
@@ -431,17 +433,20 @@ struct ConfigureAgentsViewV2: View {
431433
GroupBox {
432434
let hasOverride = ChannelStore.hasOverride(for: agent)
433435
VStack(alignment: .leading, spacing: 6) {
434-
Toggle("Use custom channels (override global)", isOn: Binding(
435-
get: { hasOverride },
436-
set: { on in
437-
if on {
438-
ChannelStore.setPerAgent(agent, config: channelConfig.global)
439-
} else {
440-
ChannelStore.clearOverride(for: agent)
436+
HStack(spacing: 6) {
437+
Toggle("Use custom channels (override global)", isOn: Binding(
438+
get: { hasOverride },
439+
set: { on in
440+
if on {
441+
ChannelStore.setPerAgent(agent, config: channelConfig.global)
442+
} else {
443+
ChannelStore.clearOverride(for: agent)
444+
}
445+
channelConfig = ChannelStore.load()
441446
}
442-
channelConfig = ChannelStore.load()
443-
}
444-
))
447+
))
448+
HelpTip("By default this agent uses the global channel settings from the Notification Channels tab. Enable this to set macOS and iPhone channels independently for just this agent.")
449+
}
445450

446451
if hasOverride {
447452
let override = channelConfig.perAgent[agent.rawValue] ?? channelConfig.global
@@ -636,6 +641,7 @@ struct ConfigureAgentsViewV2: View {
636641
: "Connecting to iCloud…")
637642
.font(.callout)
638643
.foregroundStyle(.secondary)
644+
HelpTip("Must show green for iPhone mirroring to work. If it stays grey, make sure you're signed in to iCloud in System Settings and that iCloud Drive is enabled.")
639645
Spacer()
640646
}
641647

@@ -678,14 +684,21 @@ struct ConfigureAgentsViewV2: View {
678684
// Notification event preferences
679685
GroupBox {
680686
VStack(alignment: .leading, spacing: 6) {
681-
notifPrefToggle("Session completed", $notifPrefs.sessionEnd)
682-
notifPrefToggle("Errors", $notifPrefs.error)
683-
notifPrefToggle("Permission requests", $notifPrefs.permissionNeeded)
684-
notifPrefToggle("Agent responses", $notifPrefs.agentResponse)
687+
notifPrefToggle("Session completed", $notifPrefs.sessionEnd,
688+
tip: "Notifies when an agent finishes its task successfully. On by default.")
689+
notifPrefToggle("Errors", $notifPrefs.error,
690+
tip: "Notifies on tool errors, permission errors, or aborted runs. On by default.")
691+
notifPrefToggle("Permission requests", $notifPrefs.permissionNeeded,
692+
tip: "Notifies when an agent is waiting for you to approve a tool call (e.g. Claude elicitations). On by default.")
693+
notifPrefToggle("Agent responses", $notifPrefs.agentResponse,
694+
tip: "Notifies each time the agent sends a reply. Verbose — off by default.")
685695
Divider()
686-
notifPrefToggle("Session started", $notifPrefs.sessionStart)
687-
notifPrefToggle("Sub-agent activity", $notifPrefs.subagentStart)
688-
notifPrefToggle("Tool usage", $notifPrefs.toolUse)
696+
notifPrefToggle("Session started", $notifPrefs.sessionStart,
697+
tip: "Notifies at the very beginning of a new agent session. Verbose — off by default.")
698+
notifPrefToggle("Sub-agent activity", $notifPrefs.subagentStart,
699+
tip: "Notifies when the agent spawns sub-agents or parallel tasks. Off by default.")
700+
notifPrefToggle("Tool usage", $notifPrefs.toolUse,
701+
tip: "Notifies on every file read/write/run tool call. Very verbose — off by default.")
689702
}
690703
} label: {
691704
Label("Notify me when…", systemImage: "bell.badge")
@@ -960,16 +973,21 @@ struct ConfigureAgentsViewV2: View {
960973
}
961974
}
962975

963-
private func notifPrefToggle(_ label: String, _ binding: Binding<Bool>) -> some View {
964-
Toggle(label, isOn: Binding(
965-
get: { binding.wrappedValue },
966-
set: { v in
967-
binding.wrappedValue = v
968-
ChannelStore.savePrefs(notifPrefs)
976+
private func notifPrefToggle(_ label: String, _ binding: Binding<Bool>, tip: String? = nil) -> some View {
977+
HStack(spacing: 6) {
978+
Toggle(label, isOn: Binding(
979+
get: { binding.wrappedValue },
980+
set: { v in
981+
binding.wrappedValue = v
982+
ChannelStore.savePrefs(notifPrefs)
983+
}
984+
))
985+
.toggleStyle(.checkbox)
986+
.font(.callout)
987+
if let tip {
988+
HelpTip(tip)
969989
}
970-
))
971-
.toggleStyle(.checkbox)
972-
.font(.callout)
990+
}
973991
}
974992

975993
private func resultMessage(_ r: Result<Void, Error>, verb: String) -> String {

DoomCoder/AgentTracking/TrackAgentsPopover.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ struct TrackAgentsView: View {
4545
.foregroundStyle(.secondary)
4646
Text("Track Agents").font(.headline)
4747
Spacer()
48+
HelpTip("Pauses all agent notifications until you un-pause or restart DoomCoder. Hook events are still recorded — only the notifications are suppressed. The sleep blocker keeps running.")
4849
Toggle("Paused", isOn: Binding(
4950
get: { pausedFlag },
5051
set: { on in PauseFlag.set(on); pausedFlag = on }

DoomCoder/ConfigureSettingsPane.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ struct ConfigureSettingsPane: View {
5757
Stepper(value: $sleepManager.screenOffRearmMinutes, in: 1...60) {
5858
HStack {
5959
Text("Re-sleep display after")
60+
HelpTip("After you move the mouse to wake the display in Screen Off mode, DoomCoder will put it back to sleep again after this many minutes of idle time.")
6061
Spacer()
6162
Text("\(sleepManager.screenOffRearmMinutes) min idle")
6263
.foregroundStyle(.secondary)
@@ -73,6 +74,7 @@ struct ConfigureSettingsPane: View {
7374
Stepper(value: $autoRevertSeconds, in: 10...120, step: 5) {
7475
HStack {
7576
Text("Auto-revert completed sessions to idle after")
77+
HelpTip("How long the 'completed' or 'failed' status badge stays on an agent row before it automatically reverts to 'idle'. Increase this if you miss notifications.")
7678
Spacer()
7779
Text("\(autoRevertSeconds)s")
7880
.foregroundStyle(.secondary)
@@ -92,10 +94,13 @@ struct ConfigureSettingsPane: View {
9294

9395
GroupBox {
9496
VStack(alignment: .leading, spacing: 10) {
95-
Toggle("Redact prompt text in local history", isOn: $redact)
96-
.onChange(of: redact) { _, new in
97-
UserDefaults.standard.set(new, forKey: "doomcoder.agents.redact")
98-
}
97+
HStack(spacing: 6) {
98+
Toggle("Redact prompt text in local history", isOn: $redact)
99+
.onChange(of: redact) { _, new in
100+
UserDefaults.standard.set(new, forKey: "doomcoder.agents.redact")
101+
}
102+
HelpTip("Hides agent prompt and response content in the local event log and Logs view. Event type, timing, and status are still recorded. Enabled by default for privacy.")
103+
}
99104
Divider()
100105
companionBanner
101106
}

DoomCoder/HelpTip.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import SwiftUI
2+
3+
// HelpTip — a reusable ⓘ icon that shows a plain-English explanation popover
4+
// when the user hovers over it (macOS hover-only, no tap required).
5+
//
6+
// Usage:
7+
// HStack { Text("Some Label"); HelpTip("What this control does.") }
8+
//
9+
// The popover appears automatically after a short hover delay and dismisses
10+
// when the cursor leaves. Maximum width is 260pt so text wraps cleanly.
11+
12+
struct HelpTip: View {
13+
let message: String
14+
15+
@State private var isVisible = false
16+
@State private var hoverTask: Task<Void, Never>? = nil
17+
18+
init(_ message: String) {
19+
self.message = message
20+
}
21+
22+
var body: some View {
23+
Image(systemName: "info.circle")
24+
.font(.system(size: 11, weight: .regular))
25+
.foregroundStyle(.tertiary)
26+
.contentShape(Circle())
27+
.onHover { hovering in
28+
hoverTask?.cancel()
29+
if hovering {
30+
hoverTask = Task {
31+
try? await Task.sleep(for: .milliseconds(300))
32+
guard !Task.isCancelled else { return }
33+
await MainActor.run {
34+
withAnimation(DCAnim.fade) { isVisible = true }
35+
}
36+
}
37+
} else {
38+
withAnimation(DCAnim.fade) { isVisible = false }
39+
}
40+
}
41+
.popover(isPresented: $isVisible, arrowEdge: .bottom) {
42+
HelpTipPopover(message: message)
43+
}
44+
}
45+
}
46+
47+
// MARK: - Popover content
48+
49+
private struct HelpTipPopover: View {
50+
let message: String
51+
52+
var body: some View {
53+
Text(message)
54+
.font(.callout)
55+
.foregroundStyle(.primary)
56+
.fixedSize(horizontal: false, vertical: true)
57+
.frame(maxWidth: 260, alignment: .leading)
58+
.padding(12)
59+
}
60+
}
61+
62+
// MARK: - Preview
63+
64+
#if DEBUG
65+
#Preview {
66+
HStack(spacing: 6) {
67+
Text("Screen Off")
68+
HelpTip("Display sleeps after a short delay; Mac CPU stays awake. Saves power and reduces screen burn.")
69+
}
70+
.padding(20)
71+
}
72+
#endif

DoomCoder/PanelRootView.swift

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ struct PanelRootView: View {
162162
.contentTransition(.interpolate)
163163
}
164164
Spacer()
165+
HelpTip("Master on/off switch. When turned off, the sleep blocker stops and all agent notifications are suspended. Everything resumes when you turn it back on.")
165166
Toggle("", isOn: Binding(
166167
get: { masterEnabled },
167168
set: { on in
@@ -210,22 +211,28 @@ struct PanelRootView: View {
210211
}
211212
Spacer()
212213
if sleepManager.isActive, !compactElapsed.isEmpty {
213-
Text(compactElapsed)
214-
.font(.caption2.monospacedDigit())
215-
.foregroundStyle(.tertiary)
216-
.padding(.horizontal, 7)
217-
.padding(.vertical, 3)
218-
.background(Capsule().fill(Color.white.opacity(0.06)))
219-
.contentTransition(.numericText())
214+
HStack(spacing: 4) {
215+
Text(compactElapsed)
216+
.font(.caption2.monospacedDigit())
217+
.foregroundStyle(.tertiary)
218+
.contentTransition(.numericText())
219+
HelpTip("Time elapsed since the sleep blocker was started this session.")
220+
}
221+
.padding(.horizontal, 7)
222+
.padding(.vertical, 3)
223+
.background(Capsule().fill(Color.white.opacity(0.06)))
220224
}
221225
}
222226

223227
// Mode section
224228
VStack(alignment: .leading, spacing: 6) {
225-
Text("MODE")
226-
.font(.system(size: 9, weight: .semibold))
227-
.tracking(0.6)
228-
.foregroundStyle(.tertiary)
229+
HStack(spacing: 5) {
230+
Text("MODE")
231+
.font(.system(size: 9, weight: .semibold))
232+
.tracking(0.6)
233+
.foregroundStyle(.tertiary)
234+
HelpTip("Screen On keeps the display fully lit the whole time. Screen Off lets the display sleep after a short delay while the Mac CPU stays awake — saves power and reduces screen burn.")
235+
}
229236
ModeSegmentedControl(mode: Binding(
230237
get: { sleepManager.mode },
231238
set: { newMode in
@@ -246,6 +253,7 @@ struct PanelRootView: View {
246253
.font(.system(size: 9, weight: .semibold))
247254
.tracking(0.6)
248255
.foregroundStyle(.tertiary)
256+
HelpTip("Auto-disables the sleep blocker after the chosen time. Pick 'None' to run indefinitely until you stop it manually. Tap a duration tile to start; tap the stop tile to stop early.")
249257
Spacer()
250258
Text(durationSubtitle)
251259
.font(.caption2)

0 commit comments

Comments
 (0)