detached b5fd841

refactor(daemon): clarify runtime config and palette execution names and test process inspector options

commit
b5fd841b7ba0cc5c487fea9d2211ed384696208e
author
brice · · 2026-10-08 06:23 UTC
committer
brice · · 2026-10-08 08:12 UTC
parent
4d933cb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
refactor/daemon-split
10 files · +126 -48
M internal/usecase/daemon/client.go +4 -1
@@ -25,7 +25,10 @@
2525 // it, and the capture, queue, and worker code never takes d.mu. The only nested
2626 // acquisition is a session's snapshotMu inside it (enqueueFinalSnapshotCapture
2727 // finishes a replaced capture), so snapshotMu holders must release snapshotMu
28-// before taking snapshotWorker.mu, as finishSnapshotCapture does.
28+// before taking snapshotWorker.mu, as finishSnapshotCapture does. That nested
29+// finishSnapshotCapture(replaced, false) cannot re-enter snapshotWorker.mu:
30+// final-queue captures are never normalWorkerAdmitted, and succeeded=false
31+// suppresses the forced-successor scheduling path.
2932 // snapshotWorker.noticeMu guards the active persistence-failure signature; it
3033 // is a leaf and is never held together with snapshotWorker.mu. Restoration
3134 // takes d.mu only outside snapshotWorker.mu.
@@ -25,7 +25,10 @@
25 // it, and the capture, queue, and worker code never takes d.mu. The only nested25 // it, and the capture, queue, and worker code never takes d.mu. The only nested
26 // acquisition is a session's snapshotMu inside it (enqueueFinalSnapshotCapture26 // acquisition is a session's snapshotMu inside it (enqueueFinalSnapshotCapture
27 // finishes a replaced capture), so snapshotMu holders must release snapshotMu27 // finishes a replaced capture), so snapshotMu holders must release snapshotMu
28-// before taking snapshotWorker.mu, as finishSnapshotCapture does.28+// before taking snapshotWorker.mu, as finishSnapshotCapture does. That nested
29+// finishSnapshotCapture(replaced, false) cannot re-enter snapshotWorker.mu:
30+// final-queue captures are never normalWorkerAdmitted, and succeeded=false
31+// suppresses the forced-successor scheduling path.
29 // snapshotWorker.noticeMu guards the active persistence-failure signature; it32 // snapshotWorker.noticeMu guards the active persistence-failure signature; it
30 // is a leaf and is never held together with snapshotWorker.mu. Restoration33 // is a leaf and is never held together with snapshotWorker.mu. Restoration
31 // takes d.mu only outside snapshotWorker.mu.34 // takes d.mu only outside snapshotWorker.mu.
M internal/usecase/daemon/config.go +11 -11
@@ -63,9 +63,9 @@ func defaultRuntimeConfig() *runtimeConfig {
6363 }
6464 }
6565
66-// runtimeConfig returns the current snapshot, or the defaults when none has
66+// currentRuntimeConfig returns the current snapshot, or the defaults when none has
6767 // been published (zero-value Daemon). The result is never nil.
68-func (d *Daemon) runtimeConfig() *runtimeConfig {
68+func (d *Daemon) currentRuntimeConfig() *runtimeConfig {
6969 if cfg := d.config.Load(); cfg != nil {
7070 return cfg
7171 }
@@ -73,7 +73,7 @@ func (d *Daemon) runtimeConfig() *runtimeConfig {
7373 }
7474
7575 func (d *Daemon) currentThemeConfig() themeConfigSnapshot {
76- return d.runtimeConfig().theme
76+ return d.currentRuntimeConfig().theme
7777 }
7878
7979 // ApplyConfig validates and atomically swaps daemon runtime configuration.
@@ -278,7 +278,7 @@ func buildRestoreProcessAllowlist(values []string) map[string]struct{} {
278278 }
279279
280280 func (d *Daemon) restoreProcessAllowlistSnapshot() map[string]struct{} {
281- return d.runtimeConfig().restoreProcessAllowlist
281+ return d.currentRuntimeConfig().restoreProcessAllowlist
282282 }
283283
284284 func (d *Daemon) logConfigWarning(w domain.Warning) {
@@ -290,31 +290,31 @@ func (d *Daemon) logConfigWarning(w domain.Warning) {
290290 }
291291
292292 func (d *Daemon) currentCopyConfig() domain.CopyConfig {
293- return d.runtimeConfig().copy
293+ return d.currentRuntimeConfig().copy
294294 }
295295
296296 func (d *Daemon) currentPaletteConfig() domain.PaletteConfig {
297- return d.runtimeConfig().palette
297+ return d.currentRuntimeConfig().palette
298298 }
299299
300300 func (d *Daemon) currentNavConfig() domain.NavConfig {
301- return d.runtimeConfig().nav
301+ return d.currentRuntimeConfig().nav
302302 }
303303
304304 func (d *Daemon) currentTabsConfig() domain.TabsConfig {
305- return d.runtimeConfig().tabs
305+ return d.currentRuntimeConfig().tabs
306306 }
307307
308308 func (d *Daemon) currentEphemeralConfig() domain.EphemeralConfig {
309- return d.runtimeConfig().ephemeral
309+ return d.currentRuntimeConfig().ephemeral
310310 }
311311
312312 func (d *Daemon) currentFloatingConfig() domain.FloatingConfig {
313- return d.runtimeConfig().floating
313+ return d.currentRuntimeConfig().floating
314314 }
315315
316316 func (d *Daemon) codeOverrideSnapshot() map[string]string {
317- return d.runtimeConfig().codeOverrides
317+ return d.currentRuntimeConfig().codeOverrides
318318 }
319319
320320 func commandWithOverrides(cmd command.Command, overrides map[string]string) command.Command {
@@ -63,9 +63,9 @@ func defaultRuntimeConfig() *runtimeConfig {
63 }63 }
64 }64 }
65 65
66-// runtimeConfig returns the current snapshot, or the defaults when none has66+// currentRuntimeConfig returns the current snapshot, or the defaults when none has
67 // been published (zero-value Daemon). The result is never nil.67 // been published (zero-value Daemon). The result is never nil.
68-func (d *Daemon) runtimeConfig() *runtimeConfig {68+func (d *Daemon) currentRuntimeConfig() *runtimeConfig {
69 if cfg := d.config.Load(); cfg != nil {69 if cfg := d.config.Load(); cfg != nil {
70 return cfg70 return cfg
71 }71 }
@@ -73,7 +73,7 @@ func (d *Daemon) runtimeConfig() *runtimeConfig {
73 }73 }
74 74
75 func (d *Daemon) currentThemeConfig() themeConfigSnapshot {75 func (d *Daemon) currentThemeConfig() themeConfigSnapshot {
76- return d.runtimeConfig().theme76+ return d.currentRuntimeConfig().theme
77 }77 }
78 78
79 // ApplyConfig validates and atomically swaps daemon runtime configuration.79 // ApplyConfig validates and atomically swaps daemon runtime configuration.
@@ -278,7 +278,7 @@ func buildRestoreProcessAllowlist(values []string) map[string]struct{} {
278 }278 }
279 279
280 func (d *Daemon) restoreProcessAllowlistSnapshot() map[string]struct{} {280 func (d *Daemon) restoreProcessAllowlistSnapshot() map[string]struct{} {
281- return d.runtimeConfig().restoreProcessAllowlist281+ return d.currentRuntimeConfig().restoreProcessAllowlist
282 }282 }
283 283
284 func (d *Daemon) logConfigWarning(w domain.Warning) {284 func (d *Daemon) logConfigWarning(w domain.Warning) {
@@ -290,31 +290,31 @@ func (d *Daemon) logConfigWarning(w domain.Warning) {
290 }290 }
291 291
292 func (d *Daemon) currentCopyConfig() domain.CopyConfig {292 func (d *Daemon) currentCopyConfig() domain.CopyConfig {
293- return d.runtimeConfig().copy293+ return d.currentRuntimeConfig().copy
294 }294 }
295 295
296 func (d *Daemon) currentPaletteConfig() domain.PaletteConfig {296 func (d *Daemon) currentPaletteConfig() domain.PaletteConfig {
297- return d.runtimeConfig().palette297+ return d.currentRuntimeConfig().palette
298 }298 }
299 299
300 func (d *Daemon) currentNavConfig() domain.NavConfig {300 func (d *Daemon) currentNavConfig() domain.NavConfig {
301- return d.runtimeConfig().nav301+ return d.currentRuntimeConfig().nav
302 }302 }
303 303
304 func (d *Daemon) currentTabsConfig() domain.TabsConfig {304 func (d *Daemon) currentTabsConfig() domain.TabsConfig {
305- return d.runtimeConfig().tabs305+ return d.currentRuntimeConfig().tabs
306 }306 }
307 307
308 func (d *Daemon) currentEphemeralConfig() domain.EphemeralConfig {308 func (d *Daemon) currentEphemeralConfig() domain.EphemeralConfig {
309- return d.runtimeConfig().ephemeral309+ return d.currentRuntimeConfig().ephemeral
310 }310 }
311 311
312 func (d *Daemon) currentFloatingConfig() domain.FloatingConfig {312 func (d *Daemon) currentFloatingConfig() domain.FloatingConfig {
313- return d.runtimeConfig().floating313+ return d.currentRuntimeConfig().floating
314 }314 }
315 315
316 func (d *Daemon) codeOverrideSnapshot() map[string]string {316 func (d *Daemon) codeOverrideSnapshot() map[string]string {
317- return d.runtimeConfig().codeOverrides317+ return d.currentRuntimeConfig().codeOverrides
318 }318 }
319 319
320 func commandWithOverrides(cmd command.Command, overrides map[string]string) command.Command {320 func commandWithOverrides(cmd command.Command, overrides map[string]string) command.Command {
M internal/usecase/daemon/config_test.go +2 -2
@@ -167,7 +167,7 @@ func TestEffectiveThemePaletteGate(t *testing.T) {
167167 // updateRuntimeConfig publishes a modified copy of the current runtime config
168168 // snapshot, the way ApplyConfig does, without touching unrelated sections.
169169 func updateRuntimeConfig(d *Daemon, mutate func(*runtimeConfig)) {
170- next := *d.runtimeConfig()
170+ next := *d.currentRuntimeConfig()
171171 mutate(&next)
172172 d.config.Store(&next)
173173 }
@@ -492,7 +492,7 @@ func TestPaletteCommandsUseOverrideSnapshotForRecentAndListing(t *testing.T) {
492492 "split-right": "SRX",
493493 },
494494 })
495- d.recordPaletteUse("SRX")
495+ d.paletteHistory.record("SRX")
496496
497497 commands := d.paletteCommands()
498498 require.NotEmpty(t, commands)
@@ -167,7 +167,7 @@ func TestEffectiveThemePaletteGate(t *testing.T) {
167 // updateRuntimeConfig publishes a modified copy of the current runtime config167 // updateRuntimeConfig publishes a modified copy of the current runtime config
168 // snapshot, the way ApplyConfig does, without touching unrelated sections.168 // snapshot, the way ApplyConfig does, without touching unrelated sections.
169 func updateRuntimeConfig(d *Daemon, mutate func(*runtimeConfig)) {169 func updateRuntimeConfig(d *Daemon, mutate func(*runtimeConfig)) {
170- next := *d.runtimeConfig()170+ next := *d.currentRuntimeConfig()
171 mutate(&next)171 mutate(&next)
172 d.config.Store(&next)172 d.config.Store(&next)
173 }173 }
@@ -492,7 +492,7 @@ func TestPaletteCommandsUseOverrideSnapshotForRecentAndListing(t *testing.T) {
492 "split-right": "SRX",492 "split-right": "SRX",
493 },493 },
494 })494 })
495- d.recordPaletteUse("SRX")495+ d.paletteHistory.record("SRX")
496 496
497 commands := d.paletteCommands()497 commands := d.paletteCommands()
498 require.NotEmpty(t, commands)498 require.NotEmpty(t, commands)
M internal/usecase/daemon/daemon.go +3 -2
@@ -193,8 +193,9 @@ type Daemon struct {
193193 // bindings is read by keys.Router through its own atomic pointer, so it is
194194 // published next to (not inside) the runtime config snapshot.
195195 bindings atomic.Pointer[keys.Bindings]
196- // config is the immutable runtime configuration snapshot built and
197- // stored once by ApplyConfig; read it through runtimeConfig.
196+ // config is the immutable runtime configuration snapshot, published with
197+ // defaults by New and replaced by every ApplyConfig; read it through
198+ // currentRuntimeConfig.
198199 config atomic.Pointer[runtimeConfig]
199200 barScripts *barScriptState
200201 notices *noticeCenter
@@ -193,8 +193,9 @@ type Daemon struct {
193 // bindings is read by keys.Router through its own atomic pointer, so it is193 // bindings is read by keys.Router through its own atomic pointer, so it is
194 // published next to (not inside) the runtime config snapshot.194 // published next to (not inside) the runtime config snapshot.
195 bindings atomic.Pointer[keys.Bindings]195 bindings atomic.Pointer[keys.Bindings]
196- // config is the immutable runtime configuration snapshot built and196+ // config is the immutable runtime configuration snapshot, published with
197- // stored once by ApplyConfig; read it through runtimeConfig.197+ // defaults by New and replaced by every ApplyConfig; read it through
198+ // currentRuntimeConfig.
198 config atomic.Pointer[runtimeConfig]199 config atomic.Pointer[runtimeConfig]
199 barScripts *barScriptState200 barScripts *barScriptState
200 notices *noticeCenter201 notices *noticeCenter
M internal/usecase/daemon/history_policy.go +1 -1
@@ -16,7 +16,7 @@ func historyConfigFor(policy domain.ScrollbackConfig) vt.HistoryConfig {
1616 }
1717
1818 func (d *Daemon) currentHistoryConfig() vt.HistoryConfig {
19- return historyConfigFor(d.runtimeConfig().scrollback)
19+ return historyConfigFor(d.currentRuntimeConfig().scrollback)
2020 }
2121
2222 // historyPanes gathers references without holding a session/tab lock while
@@ -16,7 +16,7 @@ func historyConfigFor(policy domain.ScrollbackConfig) vt.HistoryConfig {
16 }16 }
17 17
18 func (d *Daemon) currentHistoryConfig() vt.HistoryConfig {18 func (d *Daemon) currentHistoryConfig() vt.HistoryConfig {
19- return historyConfigFor(d.runtimeConfig().scrollback)19+ return historyConfigFor(d.currentRuntimeConfig().scrollback)
20 }20 }
21 21
22 // historyPanes gathers references without holding a session/tab lock while22 // historyPanes gathers references without holding a session/tab lock while
M internal/usecase/daemon/palette.go +14 -23
@@ -351,10 +351,6 @@ func (d *Daemon) paletteCommands() []command.Command {
351351 return out
352352 }
353353
354-func (d *Daemon) recordPaletteUse(code string) {
355- d.paletteHistory.record(code)
356-}
357-
358354 // paletteDecisionKind names what handlePaletteInput must do once the palette
359355 // lock is released.
360356 type paletteDecisionKind int
@@ -389,8 +385,8 @@ type paletteDecision struct {
389385 hasCancelInventory bool
390386 }
391387
392-// decidePaletteInput applies data to the attachment's palette while holding
393-// paletteMu and returns what the caller must do after the lock is released.
388+// decidePaletteInput takes paletteMu, applies data to the attachment's palette,
389+// and returns the action to run after unlocking.
394390 func decidePaletteInput(ac *attachedClient, data []byte) paletteDecision {
395391 var cmd command.Command
396392 var sessionTarget palette.Result
@@ -590,14 +586,13 @@ func (d *Daemon) executePaletteDecision(ac *attachedClient, entry *session, effe
590586 case paletteDecisionSessionTarget:
591587 d.executePaletteSessionTarget(ac, entry, effect, generation, rawQuery, decision.sessionTarget)
592588 case paletteDecisionCommand:
593- d.executePaletteCommand(ac, entry, effect, generation, rawQuery, decision)
589+ d.executePaletteCommand(ac, entry, effect, generation, rawQuery, decision.cmd, decision.args, decision.routeSnapshot)
594590 }
595591 }
596592
597593 func (d *Daemon) executePaletteCreateDestination(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, createDestination palette.Result) {
598- sess := entry
599594 name, _, _, _, _, _ := createDestination.CreateSessionDestination()
600- exec := paletteExec{d: d, sess: sess, attachment: entry, ac: ac, effect: effect}
595+ exec := paletteExec{d: d, sess: entry, attachment: entry, ac: ac, effect: effect}
601596 if err := exec.validateCreateSessionDestination(effect, createDestination); err != nil {
602597 ac.paletteFailure(generation, rawQuery, errCreateDestinationUnavailable.Error())
603598 d.invalidateRender(entry, ac, true, "palette.go")
@@ -666,8 +661,7 @@ func (d *Daemon) executePaletteImportedSession(ac *attachedClient, entry *sessio
666661 }
667662
668663 func (d *Daemon) executePaletteRouteNavigation(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, routeTarget protocol.RouteNavigationAction) {
669- sess := entry
670- exec := paletteExec{d: d, sess: sess, attachment: entry, ac: ac, effect: effect}
664+ exec := paletteExec{d: d, sess: entry, attachment: entry, ac: ac, effect: effect}
671665 if err := exec.NavigateRecentRoute(routeTarget); err != nil {
672666 if errors.Is(err, errAttachmentTransition) {
673667 return
@@ -682,7 +676,6 @@ func (d *Daemon) executePaletteRouteNavigation(ac *attachedClient, entry *sessio
682676 }
683677
684678 func (d *Daemon) executePaletteSessionTarget(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, sessionTarget palette.Result) {
685- sess := entry
686679 createdAt, _ := sessionTarget.SessionCreatedAt()
687680 expectedCreatedAt := createdAt.UnixNano()
688681 name, _ := sessionTarget.SessionName()
@@ -695,7 +688,7 @@ func (d *Daemon) executePaletteSessionTarget(ac *attachedClient, entry *session,
695688 if effect != nil {
696689 err = d.switchToTargetForAttachment(effect, target, sessionHandoffGuard{allowSamePeer: true}, "palette-session")
697690 } else {
698- err = d.switchToTarget(sess, ac, target)
691+ err = d.switchToTarget(entry, ac, target)
699692 }
700693 if errors.Is(err, errAttachmentTransition) {
701694 return
@@ -712,9 +705,7 @@ func (d *Daemon) executePaletteSessionTarget(ac *attachedClient, entry *session,
712705 }
713706 }
714707
715-func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, decision paletteDecision) {
716- sess := entry
717- cmd, args, routeSnapshot := decision.cmd, decision.args, decision.routeSnapshot
708+func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, cmd command.Command, args []string, routeSnapshot protocol.RecentRouteSnapshot) {
718709 if cmd.Slug == "jump-recent-session" {
719710 rank, err := command.ParsePositiveDecimal(args)
720711 if err != nil {
@@ -722,7 +713,7 @@ func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effec
722713 d.invalidateRender(entry, ac, true, "palette.go")
723714 return
724715 }
725- exec := paletteExec{d: d, sess: sess, attachment: entry, ac: ac, routeSnapshot: routeSnapshot, effect: effect}
716+ exec := paletteExec{d: d, sess: entry, attachment: entry, ac: ac, routeSnapshot: routeSnapshot, effect: effect}
726717 if err := exec.JumpRecentSession(rank); err != nil {
727718 if errors.Is(err, errAttachmentTransition) {
728719 return
@@ -732,7 +723,7 @@ func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effec
732723 return
733724 }
734725 if d.closeExecutedPalette(ac, effect, generation, rawQuery) {
735- d.recordPaletteUse(cmd.Code)
726+ d.paletteHistory.record(cmd.Code)
736727 if current := ac.currentAttachmentSession(); current != nil {
737728 d.invalidateRender(current, ac, true, "palette.go")
738729 }
@@ -743,9 +734,9 @@ func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effec
743734 if !attachmentHandoff && !d.closeExecutedPalette(ac, effect, generation, rawQuery) {
744735 return
745736 }
746- sess.dispatchMu.Lock()
747- err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac, effect: effect, redrawClosedPalette: true}, args)
748- sess.dispatchMu.Unlock()
737+ entry.dispatchMu.Lock()
738+ err := cmd.Run(paletteExec{d: d, sess: entry, ac: ac, effect: effect, redrawClosedPalette: true}, args)
739+ entry.dispatchMu.Unlock()
749740 if attachmentHandoff {
750741 if current := ac.currentSession(); current != nil {
751742 currentToken := current.captureAttachmentCapability(ac, ac.transport())
@@ -765,9 +756,9 @@ func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effec
765756 }
766757 if err != nil {
767758 d.log.Error("palette command failed", "err", err, "command", cmd.Code)
768- d.reportError(sess, paletteCommandNoticeError(cmd, err))
759+ d.reportError(entry, paletteCommandNoticeError(cmd, err))
769760 } else {
770- d.recordPaletteUse(cmd.Code)
761+ d.paletteHistory.record(cmd.Code)
771762 }
772763 }
773764
@@ -351,10 +351,6 @@ func (d *Daemon) paletteCommands() []command.Command {
351 return out351 return out
352 }352 }
353 353
354-func (d *Daemon) recordPaletteUse(code string) {
355- d.paletteHistory.record(code)
356-}
357-
358 // paletteDecisionKind names what handlePaletteInput must do once the palette354 // paletteDecisionKind names what handlePaletteInput must do once the palette
359 // lock is released.355 // lock is released.
360 type paletteDecisionKind int356 type paletteDecisionKind int
@@ -389,8 +385,8 @@ type paletteDecision struct {
389 hasCancelInventory bool385 hasCancelInventory bool
390 }386 }
391 387
392-// decidePaletteInput applies data to the attachment's palette while holding388+// decidePaletteInput takes paletteMu, applies data to the attachment's palette,
393-// paletteMu and returns what the caller must do after the lock is released.389+// and returns the action to run after unlocking.
394 func decidePaletteInput(ac *attachedClient, data []byte) paletteDecision {390 func decidePaletteInput(ac *attachedClient, data []byte) paletteDecision {
395 var cmd command.Command391 var cmd command.Command
396 var sessionTarget palette.Result392 var sessionTarget palette.Result
@@ -590,14 +586,13 @@ func (d *Daemon) executePaletteDecision(ac *attachedClient, entry *session, effe
590 case paletteDecisionSessionTarget:586 case paletteDecisionSessionTarget:
591 d.executePaletteSessionTarget(ac, entry, effect, generation, rawQuery, decision.sessionTarget)587 d.executePaletteSessionTarget(ac, entry, effect, generation, rawQuery, decision.sessionTarget)
592 case paletteDecisionCommand:588 case paletteDecisionCommand:
593- d.executePaletteCommand(ac, entry, effect, generation, rawQuery, decision)589+ d.executePaletteCommand(ac, entry, effect, generation, rawQuery, decision.cmd, decision.args, decision.routeSnapshot)
594 }590 }
595 }591 }
596 592
597 func (d *Daemon) executePaletteCreateDestination(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, createDestination palette.Result) {593 func (d *Daemon) executePaletteCreateDestination(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, createDestination palette.Result) {
598- sess := entry
599 name, _, _, _, _, _ := createDestination.CreateSessionDestination()594 name, _, _, _, _, _ := createDestination.CreateSessionDestination()
600- exec := paletteExec{d: d, sess: sess, attachment: entry, ac: ac, effect: effect}595+ exec := paletteExec{d: d, sess: entry, attachment: entry, ac: ac, effect: effect}
601 if err := exec.validateCreateSessionDestination(effect, createDestination); err != nil {596 if err := exec.validateCreateSessionDestination(effect, createDestination); err != nil {
602 ac.paletteFailure(generation, rawQuery, errCreateDestinationUnavailable.Error())597 ac.paletteFailure(generation, rawQuery, errCreateDestinationUnavailable.Error())
603 d.invalidateRender(entry, ac, true, "palette.go")598 d.invalidateRender(entry, ac, true, "palette.go")
@@ -666,8 +661,7 @@ func (d *Daemon) executePaletteImportedSession(ac *attachedClient, entry *sessio
666 }661 }
667 662
668 func (d *Daemon) executePaletteRouteNavigation(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, routeTarget protocol.RouteNavigationAction) {663 func (d *Daemon) executePaletteRouteNavigation(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, routeTarget protocol.RouteNavigationAction) {
669- sess := entry664+ exec := paletteExec{d: d, sess: entry, attachment: entry, ac: ac, effect: effect}
670- exec := paletteExec{d: d, sess: sess, attachment: entry, ac: ac, effect: effect}
671 if err := exec.NavigateRecentRoute(routeTarget); err != nil {665 if err := exec.NavigateRecentRoute(routeTarget); err != nil {
672 if errors.Is(err, errAttachmentTransition) {666 if errors.Is(err, errAttachmentTransition) {
673 return667 return
@@ -682,7 +676,6 @@ func (d *Daemon) executePaletteRouteNavigation(ac *attachedClient, entry *sessio
682 }676 }
683 677
684 func (d *Daemon) executePaletteSessionTarget(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, sessionTarget palette.Result) {678 func (d *Daemon) executePaletteSessionTarget(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, sessionTarget palette.Result) {
685- sess := entry
686 createdAt, _ := sessionTarget.SessionCreatedAt()679 createdAt, _ := sessionTarget.SessionCreatedAt()
687 expectedCreatedAt := createdAt.UnixNano()680 expectedCreatedAt := createdAt.UnixNano()
688 name, _ := sessionTarget.SessionName()681 name, _ := sessionTarget.SessionName()
@@ -695,7 +688,7 @@ func (d *Daemon) executePaletteSessionTarget(ac *attachedClient, entry *session,
695 if effect != nil {688 if effect != nil {
696 err = d.switchToTargetForAttachment(effect, target, sessionHandoffGuard{allowSamePeer: true}, "palette-session")689 err = d.switchToTargetForAttachment(effect, target, sessionHandoffGuard{allowSamePeer: true}, "palette-session")
697 } else {690 } else {
698- err = d.switchToTarget(sess, ac, target)691+ err = d.switchToTarget(entry, ac, target)
699 }692 }
700 if errors.Is(err, errAttachmentTransition) {693 if errors.Is(err, errAttachmentTransition) {
701 return694 return
@@ -712,9 +705,7 @@ func (d *Daemon) executePaletteSessionTarget(ac *attachedClient, entry *session,
712 }705 }
713 }706 }
714 707
715-func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, decision paletteDecision) {708+func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effect *attachmentEffect, generation uint64, rawQuery string, cmd command.Command, args []string, routeSnapshot protocol.RecentRouteSnapshot) {
716- sess := entry
717- cmd, args, routeSnapshot := decision.cmd, decision.args, decision.routeSnapshot
718 if cmd.Slug == "jump-recent-session" {709 if cmd.Slug == "jump-recent-session" {
719 rank, err := command.ParsePositiveDecimal(args)710 rank, err := command.ParsePositiveDecimal(args)
720 if err != nil {711 if err != nil {
@@ -722,7 +713,7 @@ func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effec
722 d.invalidateRender(entry, ac, true, "palette.go")713 d.invalidateRender(entry, ac, true, "palette.go")
723 return714 return
724 }715 }
725- exec := paletteExec{d: d, sess: sess, attachment: entry, ac: ac, routeSnapshot: routeSnapshot, effect: effect}716+ exec := paletteExec{d: d, sess: entry, attachment: entry, ac: ac, routeSnapshot: routeSnapshot, effect: effect}
726 if err := exec.JumpRecentSession(rank); err != nil {717 if err := exec.JumpRecentSession(rank); err != nil {
727 if errors.Is(err, errAttachmentTransition) {718 if errors.Is(err, errAttachmentTransition) {
728 return719 return
@@ -732,7 +723,7 @@ func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effec
732 return723 return
733 }724 }
734 if d.closeExecutedPalette(ac, effect, generation, rawQuery) {725 if d.closeExecutedPalette(ac, effect, generation, rawQuery) {
735- d.recordPaletteUse(cmd.Code)726+ d.paletteHistory.record(cmd.Code)
736 if current := ac.currentAttachmentSession(); current != nil {727 if current := ac.currentAttachmentSession(); current != nil {
737 d.invalidateRender(current, ac, true, "palette.go")728 d.invalidateRender(current, ac, true, "palette.go")
738 }729 }
@@ -743,9 +734,9 @@ func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effec
743 if !attachmentHandoff && !d.closeExecutedPalette(ac, effect, generation, rawQuery) {734 if !attachmentHandoff && !d.closeExecutedPalette(ac, effect, generation, rawQuery) {
744 return735 return
745 }736 }
746- sess.dispatchMu.Lock()737+ entry.dispatchMu.Lock()
747- err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac, effect: effect, redrawClosedPalette: true}, args)738+ err := cmd.Run(paletteExec{d: d, sess: entry, ac: ac, effect: effect, redrawClosedPalette: true}, args)
748- sess.dispatchMu.Unlock()739+ entry.dispatchMu.Unlock()
749 if attachmentHandoff {740 if attachmentHandoff {
750 if current := ac.currentSession(); current != nil {741 if current := ac.currentSession(); current != nil {
751 currentToken := current.captureAttachmentCapability(ac, ac.transport())742 currentToken := current.captureAttachmentCapability(ac, ac.transport())
@@ -765,9 +756,9 @@ func (d *Daemon) executePaletteCommand(ac *attachedClient, entry *session, effec
765 }756 }
766 if err != nil {757 if err != nil {
767 d.log.Error("palette command failed", "err", err, "command", cmd.Code)758 d.log.Error("palette command failed", "err", err, "command", cmd.Code)
768- d.reportError(sess, paletteCommandNoticeError(cmd, err))759+ d.reportError(entry, paletteCommandNoticeError(cmd, err))
769 } else {760 } else {
770- d.recordPaletteUse(cmd.Code)761+ d.paletteHistory.record(cmd.Code)
771 }762 }
772 }763 }
773 764
M internal/usecase/daemon/palette_test.go +6 -6
@@ -936,11 +936,11 @@ func TestCommandByEffectiveCodeExcludesAPIOnlyCommands(t *testing.T) {
936936
937937 func TestPaletteRecentCommandsNewestFirstThenRegistryOrder(t *testing.T) {
938938 d := &Daemon{}
939- d.recordPaletteUse("SSP")
940- d.recordPaletteUse("NXT")
939+ d.paletteHistory.record("SSP")
940+ d.paletteHistory.record("NXT")
941941 // STALE is not a registered command code; it must be dropped from output.
942- d.recordPaletteUse("STALE")
943- d.recordPaletteUse("SSP")
942+ d.paletteHistory.record("STALE")
943+ d.paletteHistory.record("SSP")
944944
945945 commands := d.paletteCommands()
946946 codes := make([]string, len(commands))
@@ -960,7 +960,7 @@ func TestPaletteRecencyCanBeUpdatedConcurrently(t *testing.T) {
960960 wg.Add(1)
961961 go func(code string) {
962962 defer wg.Done()
963- d.recordPaletteUse(code)
963+ d.paletteHistory.record(code)
964964 _ = d.paletteCommands()
965965 }(code)
966966 }
@@ -1228,7 +1228,7 @@ func TestPaletteTabCompletesSelectedCommandWithoutForwardingToPTY(t *testing.T)
12281228 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
12291229 invs := installPaletteInvalidationObserver(sess)
12301230 if tt.recent != "" {
1231- d.recordPaletteUse(tt.recent)
1231+ d.paletteHistory.record(tt.recent)
12321232 }
12331233 if tt.config != nil {
12341234 d.ApplyConfig(domain.Config{Codes: tt.config})
@@ -936,11 +936,11 @@ func TestCommandByEffectiveCodeExcludesAPIOnlyCommands(t *testing.T) {
936 936
937 func TestPaletteRecentCommandsNewestFirstThenRegistryOrder(t *testing.T) {937 func TestPaletteRecentCommandsNewestFirstThenRegistryOrder(t *testing.T) {
938 d := &Daemon{}938 d := &Daemon{}
939- d.recordPaletteUse("SSP")939+ d.paletteHistory.record("SSP")
940- d.recordPaletteUse("NXT")940+ d.paletteHistory.record("NXT")
941 // STALE is not a registered command code; it must be dropped from output.941 // STALE is not a registered command code; it must be dropped from output.
942- d.recordPaletteUse("STALE")942+ d.paletteHistory.record("STALE")
943- d.recordPaletteUse("SSP")943+ d.paletteHistory.record("SSP")
944 944
945 commands := d.paletteCommands()945 commands := d.paletteCommands()
946 codes := make([]string, len(commands))946 codes := make([]string, len(commands))
@@ -960,7 +960,7 @@ func TestPaletteRecencyCanBeUpdatedConcurrently(t *testing.T) {
960 wg.Add(1)960 wg.Add(1)
961 go func(code string) {961 go func(code string) {
962 defer wg.Done()962 defer wg.Done()
963- d.recordPaletteUse(code)963+ d.paletteHistory.record(code)
964 _ = d.paletteCommands()964 _ = d.paletteCommands()
965 }(code)965 }(code)
966 }966 }
@@ -1228,7 +1228,7 @@ func TestPaletteTabCompletesSelectedCommandWithoutForwardingToPTY(t *testing.T)
1228 d, sess, ac, sends := newManualSessionWithPTYs(t, p)1228 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
1229 invs := installPaletteInvalidationObserver(sess)1229 invs := installPaletteInvalidationObserver(sess)
1230 if tt.recent != "" {1230 if tt.recent != "" {
1231- d.recordPaletteUse(tt.recent)1231+ d.paletteHistory.record(tt.recent)
1232 }1232 }
1233 if tt.config != nil {1233 if tt.config != nil {
1234 d.ApplyConfig(domain.Config{Codes: tt.config})1234 d.ApplyConfig(domain.Config{Codes: tt.config})
A internal/usecase/daemon/process_inspector_test.go +83 -0
@@ -0,0 +1,83 @@
1+package daemon
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+)
8+
9+func TestProcessInspectorOptionsComposition(t *testing.T) {
10+ cwd := func(int) (string, error) { return "/override", nil }
11+ base := &processInspectorFake{
12+ cwd: func(int) (string, error) { return "/base", nil },
13+ comm: func(int) (string, error) { return "vim", nil },
14+ }
15+
16+ tests := []struct {
17+ name string
18+ opts []Option
19+ wantDetails bool
20+ wantCwd string
21+ wantComm string
22+ wantCommErr error
23+ }{
24+ {name: "none", wantDetails: false},
25+ {
26+ name: "cwd reader only",
27+ opts: []Option{WithCwdReader(cwd)},
28+ wantDetails: false,
29+ wantCwd: "/override",
30+ wantCommErr: errProcessInspectionUnavailable,
31+ },
32+ {
33+ name: "inspector only",
34+ opts: []Option{WithProcessInspector(base)},
35+ wantDetails: true,
36+ wantCwd: "/base",
37+ wantComm: "vim",
38+ },
39+ {
40+ name: "inspector then cwd reader keeps base details",
41+ opts: []Option{WithProcessInspector(base), WithCwdReader(cwd)},
42+ wantDetails: true,
43+ wantCwd: "/override",
44+ wantComm: "vim",
45+ },
46+ {
47+ name: "cwd reader then inspector replaces override",
48+ opts: []Option{WithCwdReader(cwd), WithProcessInspector(base)},
49+ wantDetails: true,
50+ wantCwd: "/base",
51+ wantComm: "vim",
52+ },
53+ {
54+ name: "repeated cwd readers do not stack",
55+ opts: []Option{WithProcessInspector(base), WithCwdReader(cwd), WithCwdReader(cwd)},
56+ wantDetails: true,
57+ wantCwd: "/override",
58+ wantComm: "vim",
59+ },
60+ }
61+ for _, tt := range tests {
62+ t.Run(tt.name, func(t *testing.T) {
63+ d := &Daemon{}
64+ for _, opt := range tt.opts {
65+ opt(d)
66+ }
67+ require.Equal(t, tt.wantDetails, d.hasProcessDetails())
68+ if d.proc == nil {
69+ return
70+ }
71+ got, err := d.proc.Cwd(1)
72+ require.NoError(t, err)
73+ require.Equal(t, tt.wantCwd, got)
74+ comm, err := d.proc.Comm(1)
75+ require.ErrorIs(t, err, tt.wantCommErr)
76+ require.Equal(t, tt.wantComm, comm)
77+ if o, ok := d.proc.(cwdOverrideInspector); ok {
78+ _, nested := o.base.(cwdOverrideInspector)
79+ require.False(t, nested)
80+ }
81+ })
82+ }
83+}
@@ -0,0 +1,83 @@
1+package daemon
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+)
8+
9+func TestProcessInspectorOptionsComposition(t *testing.T) {
10+ cwd := func(int) (string, error) { return "/override", nil }
11+ base := &processInspectorFake{
12+ cwd: func(int) (string, error) { return "/base", nil },
13+ comm: func(int) (string, error) { return "vim", nil },
14+ }
15+
16+ tests := []struct {
17+ name string
18+ opts []Option
19+ wantDetails bool
20+ wantCwd string
21+ wantComm string
22+ wantCommErr error
23+ }{
24+ {name: "none", wantDetails: false},
25+ {
26+ name: "cwd reader only",
27+ opts: []Option{WithCwdReader(cwd)},
28+ wantDetails: false,
29+ wantCwd: "/override",
30+ wantCommErr: errProcessInspectionUnavailable,
31+ },
32+ {
33+ name: "inspector only",
34+ opts: []Option{WithProcessInspector(base)},
35+ wantDetails: true,
36+ wantCwd: "/base",
37+ wantComm: "vim",
38+ },
39+ {
40+ name: "inspector then cwd reader keeps base details",
41+ opts: []Option{WithProcessInspector(base), WithCwdReader(cwd)},
42+ wantDetails: true,
43+ wantCwd: "/override",
44+ wantComm: "vim",
45+ },
46+ {
47+ name: "cwd reader then inspector replaces override",
48+ opts: []Option{WithCwdReader(cwd), WithProcessInspector(base)},
49+ wantDetails: true,
50+ wantCwd: "/base",
51+ wantComm: "vim",
52+ },
53+ {
54+ name: "repeated cwd readers do not stack",
55+ opts: []Option{WithProcessInspector(base), WithCwdReader(cwd), WithCwdReader(cwd)},
56+ wantDetails: true,
57+ wantCwd: "/override",
58+ wantComm: "vim",
59+ },
60+ }
61+ for _, tt := range tests {
62+ t.Run(tt.name, func(t *testing.T) {
63+ d := &Daemon{}
64+ for _, opt := range tt.opts {
65+ opt(d)
66+ }
67+ require.Equal(t, tt.wantDetails, d.hasProcessDetails())
68+ if d.proc == nil {
69+ return
70+ }
71+ got, err := d.proc.Cwd(1)
72+ require.NoError(t, err)
73+ require.Equal(t, tt.wantCwd, got)
74+ comm, err := d.proc.Comm(1)
75+ require.ErrorIs(t, err, tt.wantCommErr)
76+ require.Equal(t, tt.wantComm, comm)
77+ if o, ok := d.proc.(cwdOverrideInspector); ok {
78+ _, nested := o.base.(cwdOverrideInspector)
79+ require.False(t, nested)
80+ }
81+ })
82+ }
83+}
M internal/usecase/daemon/snapshot.go +1 -1
@@ -47,7 +47,7 @@ type snapshotCapture struct {
4747 publicationContext context.Context
4848 sealedRefs map[*vt.HistoryChunk]snapcodec.ObjectRef // set by the single encoder worker
4949 coordinatorDiscarded bool // guarded by session.snapshotMu
50- // normalWorkerAdmitted is set before snapshotJobs admission and immutable
50+ // normalWorkerAdmitted is set before snapshots.jobs admission and immutable
5151 // until completion; final-queue captures leave it false.
5252 normalWorkerAdmitted bool
5353 finishOnce sync.Once
@@ -47,7 +47,7 @@ type snapshotCapture struct {
47 publicationContext context.Context47 publicationContext context.Context
48 sealedRefs map[*vt.HistoryChunk]snapcodec.ObjectRef // set by the single encoder worker48 sealedRefs map[*vt.HistoryChunk]snapcodec.ObjectRef // set by the single encoder worker
49 coordinatorDiscarded bool // guarded by session.snapshotMu49 coordinatorDiscarded bool // guarded by session.snapshotMu
50- // normalWorkerAdmitted is set before snapshotJobs admission and immutable50+ // normalWorkerAdmitted is set before snapshots.jobs admission and immutable
51 // until completion; final-queue captures leave it false.51 // until completion; final-queue captures leave it false.
52 normalWorkerAdmitted bool52 normalWorkerAdmitted bool
53 finishOnce sync.Once53 finishOnce sync.Once
M internal/usecase/daemon/snapshot_worker.go +1 -1
@@ -275,7 +275,7 @@ func (d *Daemon) requestDurableWriterStop() (context.CancelFunc, <-chan struct{}
275275 }
276276
277277 // durableWriterFailureNames includes admitted buffered captures as well as the
278-// active and final queues. snapshotAdmitted tracks normal captures from queue
278+// active and final queues. snapshots.admitted tracks normal captures from queue
279279 // admission through completion, so worker dequeue cannot make one disappear.
280280 func (d *Daemon) durableWriterFailureNames() []string {
281281 d.snapshots.mu.Lock()
@@ -275,7 +275,7 @@ func (d *Daemon) requestDurableWriterStop() (context.CancelFunc, <-chan struct{}
275 }275 }
276 276
277 // durableWriterFailureNames includes admitted buffered captures as well as the277 // durableWriterFailureNames includes admitted buffered captures as well as the
278-// active and final queues. snapshotAdmitted tracks normal captures from queue278+// active and final queues. snapshots.admitted tracks normal captures from queue
279 // admission through completion, so worker dequeue cannot make one disappear.279 // admission through completion, so worker dequeue cannot make one disappear.
280 func (d *Daemon) durableWriterFailureNames() []string {280 func (d *Daemon) durableWriterFailureNames() []string {
281 d.snapshots.mu.Lock()281 d.snapshots.mu.Lock()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close