docs: remove client contract plans
- commit
- 80cdd12f76fdcfa29a7536fc5a70f56e61425c8f
- author
- brice · · 2026-09-23 18:57 UTC
- parent
- eabbcfe
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +0 -221
D docs/plans/client-preferences-contract.md +0 -83
| @@ -1,83 +0,0 @@ | |||
| 1 | - | # Client preferences contract (P4.1, design-only) | |
| 2 | - | ||
| 3 | - | Status: proposed. No production edits accompany this document. Any scope | |
| 4 | - | change below requires explicit approval before implementation. | |
| 5 | - | ||
| 6 | - | ## Ground rules | |
| 7 | - | ||
| 8 | - | - Shared pane VT defaults stay daemon-arbitrated. `applyHostThemeLocked` | |
| 9 | - | (`internal/usecase/daemon/theme_resolve.go`) fans the resolved theme out to | |
| 10 | - | every pane VT in every tab of the session; per-attachment values never reach | |
| 11 | - | pane screens. | |
| 12 | - | - Picker sort scope is unchanged: `togglePickerSort` flips the daemon-wide | |
| 13 | - | `pickerSort` atomic (`internal/usecase/daemon/picker.go:40`); it is session | |
| 14 | - | chrome, not an attachment preference. | |
| 15 | - | - Bindings stay daemon-admitted. `keys.BuildBindingEntries` + | |
| 16 | - | `keys.Router.RouteWithHandler` (`internal/usecase/keys`) remain the single | |
| 17 | - | admission path; no duplicate client-side key router is proposed. | |
| 18 | - | - Config source of truth stays the daemon-loaded file parsed by | |
| 19 | - | `internal/adapters/config` into `domain.Config`, published atomically via | |
| 20 | - | `Daemon.ApplyConfig` snapshots (`internal/usecase/daemon/config.go`). | |
| 21 | - | ||
| 22 | - | ## Classes | |
| 23 | - | ||
| 24 | - | - **Client-local interaction**: affects only the local terminal device, never | |
| 25 | - | pane VT state, session state, or another attachment. The only class eligible | |
| 26 | - | for client-side ownership without a P5 lease. | |
| 27 | - | - **Attachment-rendering**: affects presentation of one attachment, executed by | |
| 28 | - | the serving daemon from per-attachment state. Shared session/pane state | |
| 29 | - | untouched. | |
| 30 | - | - **Shared application-visible**: affects panes, sessions, or all attachments. | |
| 31 | - | Daemon-owned; not candidates for client ownership in this plan. | |
| 32 | - | ||
| 33 | - | ## Field classification | |
| 34 | - | ||
| 35 | - | All keys are the existing `internal/adapters/config` keys. "Two-attachment | |
| 36 | - | result" describes two simultaneous attachments A and B on the same session | |
| 37 | - | with different local wishes. | |
| 38 | - | ||
| 39 | - | | Key | Class | Source / default / reload / validation | Two-attachment result | | |
| 40 | - | |---|---|---|---| | |
| 41 | - | | `theme`, `theme.palette`, `theme.accent` | Shared | Daemon config file; defaults `auto`/`on`/`auto` (`domain.Defaults`); atomic reload via `storeThemeConfig`, reapplied with `reapplyThemeAllSessions`; invalid values warn and keep prior (`config_test.go`) | Single effective theme per session (`effectiveThemeForConfig`, `resolveAppliedTheme`); per-attachment client theme is only an input to arbitration, never applied directly to panes. **Preserved as-is.** | | |
| 42 | - | | `scrollback.*` | Shared | Daemon config; defaults 50 MiB / 10k lines; `Scrollback.Valid()` falls back to defaults with a warning | Per-pane retention policy; identical for every viewer. **Preserved as-is.** | | |
| 43 | - | | `snapshot.*` | Shared | Daemon config; allowlist snapshot via `restoreProcessAllowlistFromConfig` | Session restore; identical for every viewer. **Preserved as-is.** | | |
| 44 | - | | `floating.command`, `floating.width`, `floating.height` | Shared | Daemon config; defaults 80x80; stored via `floatingConfig` | Geometry arbitration is daemon-owned (`calculateContentFloatingGeometry`). **Preserved as-is.** | | |
| 45 | - | | `bar.top-right`, `bar.bottom-right`, `bar.interval` | Shared | Daemon config; defaults empty/empty/5s, clamped to `MinBarInterval` (1s) | Daemon-host execution, per-session outputs. Governed by the status contract; **no client execution proposed here.** | | |
| 46 | - | | `code.*`, `binding.*` | Shared | Daemon config; `buildCodeOverrides` + `BuildBindingEntries`, warn-and-keep on invalid | Daemon-admitted palette commands and key bindings. **Single router preserved.** | | |
| 47 | - | | `copy.word-separators`, `copy.reduce-motion` | Shared (daemon-executed attached interaction) | Daemon config; defaults ` -_@` / false; `currentCopyConfig` snapshot | Copy mode stays daemon-owned per plan non-goals. Selection documents (`scopy.NewDocument`) and scroll animation execute in the daemon. **Not candidates; moving them requires the P5 lease.** | | |
| 48 | - | | `nav.overflow-tabs`, `nav.overflow-sessions` | Shared (attached-action behavior) | Daemon config; defaults false; `currentNavConfig` | Directional navigation is daemon-admitted (`pane_actions.go:465`). **Not candidates.** | | |
| 49 | - | | `tabs.terminal-title` | Attachment-rendering | Daemon config; default true; `currentTabsConfig` snapshot read at status composition (`status.go:303,334`, `picker.go:96`) | Label-only; per-attachment divergence is safe in principle, but no per-attachment source exists today. **Proposed future field (example): read from attachment claim, defaulting to daemon config. Requires approval; not implemented here.** | | |
| 50 | - | | `palette.anchor` | Attachment-rendering | Daemon config; default center; `currentPaletteConfig` read at render (`render.go:443`) | Placement-only; same future-field shape as above. **Requires approval; not implemented here.** | | |
| 51 | - | ||
| 52 | - | ## Proposed fields and examples | |
| 53 | - | ||
| 54 | - | No new configuration keys are introduced by this contract. The only proposed | |
| 55 | - | shape, pending approval, is: an attachment-rendering value resolves as | |
| 56 | - | *attachment-claimed value if present, else daemon config*, read at composition | |
| 57 | - | time from existing snapshots, never written to `domain.Config` or persisted. | |
| 58 | - | `tabs.terminal-title` and `palette.anchor` are the worked examples above. | |
| 59 | - | ||
| 60 | - | Explicitly out of scope: client-local clipboard policy, client-side theme | |
| 61 | - | editing, per-attachment scrollback, per-attachment bindings, and any new | |
| 62 | - | `domain.Config` field. Each would need its own approved contract. | |
| 63 | - | ||
| 64 | - | ## Tests mapped (to be added only after approval) | |
| 65 | - | ||
| 66 | - | - `TestAttachmentRenderingPreferenceFallsBackToDaemonConfig` (daemon): | |
| 67 | - | attachment without claim renders with daemon `palette.anchor` / | |
| 68 | - | `tabs.terminal-title`. | |
| 69 | - | - `TestAttachmentRenderingPreferenceClaimWins` (daemon): claimed value used, | |
| 70 | - | shared pane VT colors byte-identical to unclaimed control. | |
| 71 | - | - `TestTwoAttachmentsIndependentRenderingPrefs` (daemon): A and B on one | |
| 72 | - | session render different anchors/titles; pane screens identical. | |
| 73 | - | - `TestInvalidRenderingPreferenceWarnsAndKeepsPrior` (config + daemon): | |
| 74 | - | warn-and-continue path mirrors existing theme/scrollback tests. | |
| 75 | - | - Existing preserved-behavior pins: `TestDefaults*`, `TestParse*` in | |
| 76 | - | `internal/adapters/config`, `config_test.go` snapshot tests, | |
| 77 | - | `picker_test.go` sort tests, `theme_integration_test.go`. | |
| 78 | - | ||
| 79 | - | ## Stop condition | |
| 80 | - | ||
| 81 | - | P4.1 ends at this document. No preference scope change, no `domain.Config` | |
| 82 | - | field, no wire field, and no client ownership move is authorized by this | |
| 83 | - | plan alone. | |
| @@ -1,83 +0,0 @@ | |||||
| 1 | - | # Client preferences contract (P4.1, design-only) | |||
| 2 | - | ||||
| 3 | - | Status: proposed. No production edits accompany this document. Any scope | |||
| 4 | - | change below requires explicit approval before implementation. | |||
| 5 | - | ||||
| 6 | - | ## Ground rules | |||
| 7 | - | ||||
| 8 | - | - Shared pane VT defaults stay daemon-arbitrated. `applyHostThemeLocked` | |||
| 9 | - | (`internal/usecase/daemon/theme_resolve.go`) fans the resolved theme out to | |||
| 10 | - | every pane VT in every tab of the session; per-attachment values never reach | |||
| 11 | - | pane screens. | |||
| 12 | - | - Picker sort scope is unchanged: `togglePickerSort` flips the daemon-wide | |||
| 13 | - | `pickerSort` atomic (`internal/usecase/daemon/picker.go:40`); it is session | |||
| 14 | - | chrome, not an attachment preference. | |||
| 15 | - | - Bindings stay daemon-admitted. `keys.BuildBindingEntries` + | |||
| 16 | - | `keys.Router.RouteWithHandler` (`internal/usecase/keys`) remain the single | |||
| 17 | - | admission path; no duplicate client-side key router is proposed. | |||
| 18 | - | - Config source of truth stays the daemon-loaded file parsed by | |||
| 19 | - | `internal/adapters/config` into `domain.Config`, published atomically via | |||
| 20 | - | `Daemon.ApplyConfig` snapshots (`internal/usecase/daemon/config.go`). | |||
| 21 | - | ||||
| 22 | - | ## Classes | |||
| 23 | - | ||||
| 24 | - | - **Client-local interaction**: affects only the local terminal device, never | |||
| 25 | - | pane VT state, session state, or another attachment. The only class eligible | |||
| 26 | - | for client-side ownership without a P5 lease. | |||
| 27 | - | - **Attachment-rendering**: affects presentation of one attachment, executed by | |||
| 28 | - | the serving daemon from per-attachment state. Shared session/pane state | |||
| 29 | - | untouched. | |||
| 30 | - | - **Shared application-visible**: affects panes, sessions, or all attachments. | |||
| 31 | - | Daemon-owned; not candidates for client ownership in this plan. | |||
| 32 | - | ||||
| 33 | - | ## Field classification | |||
| 34 | - | ||||
| 35 | - | All keys are the existing `internal/adapters/config` keys. "Two-attachment | |||
| 36 | - | result" describes two simultaneous attachments A and B on the same session | |||
| 37 | - | with different local wishes. | |||
| 38 | - | ||||
| 39 | - | | Key | Class | Source / default / reload / validation | Two-attachment result | | |||
| 40 | - | |---|---|---|---| | |||
| 41 | - | | `theme`, `theme.palette`, `theme.accent` | Shared | Daemon config file; defaults `auto`/`on`/`auto` (`domain.Defaults`); atomic reload via `storeThemeConfig`, reapplied with `reapplyThemeAllSessions`; invalid values warn and keep prior (`config_test.go`) | Single effective theme per session (`effectiveThemeForConfig`, `resolveAppliedTheme`); per-attachment client theme is only an input to arbitration, never applied directly to panes. **Preserved as-is.** | | |||
| 42 | - | | `scrollback.*` | Shared | Daemon config; defaults 50 MiB / 10k lines; `Scrollback.Valid()` falls back to defaults with a warning | Per-pane retention policy; identical for every viewer. **Preserved as-is.** | | |||
| 43 | - | | `snapshot.*` | Shared | Daemon config; allowlist snapshot via `restoreProcessAllowlistFromConfig` | Session restore; identical for every viewer. **Preserved as-is.** | | |||
| 44 | - | | `floating.command`, `floating.width`, `floating.height` | Shared | Daemon config; defaults 80x80; stored via `floatingConfig` | Geometry arbitration is daemon-owned (`calculateContentFloatingGeometry`). **Preserved as-is.** | | |||
| 45 | - | | `bar.top-right`, `bar.bottom-right`, `bar.interval` | Shared | Daemon config; defaults empty/empty/5s, clamped to `MinBarInterval` (1s) | Daemon-host execution, per-session outputs. Governed by the status contract; **no client execution proposed here.** | | |||
| 46 | - | | `code.*`, `binding.*` | Shared | Daemon config; `buildCodeOverrides` + `BuildBindingEntries`, warn-and-keep on invalid | Daemon-admitted palette commands and key bindings. **Single router preserved.** | | |||
| 47 | - | | `copy.word-separators`, `copy.reduce-motion` | Shared (daemon-executed attached interaction) | Daemon config; defaults ` -_@` / false; `currentCopyConfig` snapshot | Copy mode stays daemon-owned per plan non-goals. Selection documents (`scopy.NewDocument`) and scroll animation execute in the daemon. **Not candidates; moving them requires the P5 lease.** | | |||
| 48 | - | | `nav.overflow-tabs`, `nav.overflow-sessions` | Shared (attached-action behavior) | Daemon config; defaults false; `currentNavConfig` | Directional navigation is daemon-admitted (`pane_actions.go:465`). **Not candidates.** | | |||
| 49 | - | | `tabs.terminal-title` | Attachment-rendering | Daemon config; default true; `currentTabsConfig` snapshot read at status composition (`status.go:303,334`, `picker.go:96`) | Label-only; per-attachment divergence is safe in principle, but no per-attachment source exists today. **Proposed future field (example): read from attachment claim, defaulting to daemon config. Requires approval; not implemented here.** | | |||
| 50 | - | | `palette.anchor` | Attachment-rendering | Daemon config; default center; `currentPaletteConfig` read at render (`render.go:443`) | Placement-only; same future-field shape as above. **Requires approval; not implemented here.** | | |||
| 51 | - | ||||
| 52 | - | ## Proposed fields and examples | |||
| 53 | - | ||||
| 54 | - | No new configuration keys are introduced by this contract. The only proposed | |||
| 55 | - | shape, pending approval, is: an attachment-rendering value resolves as | |||
| 56 | - | *attachment-claimed value if present, else daemon config*, read at composition | |||
| 57 | - | time from existing snapshots, never written to `domain.Config` or persisted. | |||
| 58 | - | `tabs.terminal-title` and `palette.anchor` are the worked examples above. | |||
| 59 | - | ||||
| 60 | - | Explicitly out of scope: client-local clipboard policy, client-side theme | |||
| 61 | - | editing, per-attachment scrollback, per-attachment bindings, and any new | |||
| 62 | - | `domain.Config` field. Each would need its own approved contract. | |||
| 63 | - | ||||
| 64 | - | ## Tests mapped (to be added only after approval) | |||
| 65 | - | ||||
| 66 | - | - `TestAttachmentRenderingPreferenceFallsBackToDaemonConfig` (daemon): | |||
| 67 | - | attachment without claim renders with daemon `palette.anchor` / | |||
| 68 | - | `tabs.terminal-title`. | |||
| 69 | - | - `TestAttachmentRenderingPreferenceClaimWins` (daemon): claimed value used, | |||
| 70 | - | shared pane VT colors byte-identical to unclaimed control. | |||
| 71 | - | - `TestTwoAttachmentsIndependentRenderingPrefs` (daemon): A and B on one | |||
| 72 | - | session render different anchors/titles; pane screens identical. | |||
| 73 | - | - `TestInvalidRenderingPreferenceWarnsAndKeepsPrior` (config + daemon): | |||
| 74 | - | warn-and-continue path mirrors existing theme/scrollback tests. | |||
| 75 | - | - Existing preserved-behavior pins: `TestDefaults*`, `TestParse*` in | |||
| 76 | - | `internal/adapters/config`, `config_test.go` snapshot tests, | |||
| 77 | - | `picker_test.go` sort tests, `theme_integration_test.go`. | |||
| 78 | - | ||||
| 79 | - | ## Stop condition | |||
| 80 | - | ||||
| 81 | - | P4.1 ends at this document. No preference scope change, no `domain.Config` | |||
| 82 | - | field, no wire field, and no client ownership move is authorized by this | |||
| 83 | - | plan alone. | |||
D docs/plans/client-status-contract.md +0 -138
| @@ -1,138 +0,0 @@ | |||
| 1 | - | # Client status contract (P4.2, design-only) | |
| 2 | - | ||
| 3 | - | Status: proposed. No production edits accompany this document. No provider | |
| 4 | - | implementation is authorized by this plan alone; a runnable task breakdown | |
| 5 | - | follows only after contract approval. | |
| 6 | - | ||
| 7 | - | ## Current daemon-host behavior (preserved) | |
| 8 | - | ||
| 9 | - | - Commands come only from the daemon-loaded config: `bar.top-right`, | |
| 10 | - | `bar.bottom-right`, `bar.interval` (defaults empty/empty/5s, | |
| 11 | - | `internal/adapters/config`, `domain.Defaults`). | |
| 12 | - | - Execution is daemon-host via `barScriptRunner.run` | |
| 13 | - | (`internal/usecase/daemon/bar_script.go`) with `ports.ShellCommandRunner`: | |
| 14 | - | timeout 1s (`barScriptTimeout`), stdout cap 1024, stderr cap 512, display | |
| 15 | - | cap 256, first-line + `stripANSIAndControls` sanitization, warn-once | |
| 16 | - | failure dedup (`lastFailure`). | |
| 17 | - | - Scheduling is per-session in `bar_refresh.go`: poller on the daemon clock | |
| 18 | - | with `MinBarInterval` (1s) floor, `running`/`pending` coalescing, context | |
| 19 | - | change (`lastContext`) forces refresh, `reload` channel wakes the poller on | |
| 20 | - | config change, `refreshBarScriptsAllSessions` applies new commands | |
| 21 | - | immediately. | |
| 22 | - | - Scripts observe daemon-side context only: `VEV_ANCHOR`, `VEV_SESSION`, | |
| 23 | - | `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, `VEV_COLS`. | |
| 24 | - | - Composition reads snapshots at paint time: `barScriptSnapshot(sess)` feeds | |
| 25 | - | `barState.topRight`/`bottomRight` alongside `status`, `statusFeedback`, | |
| 26 | - | `mru`/`rankedRecent`, `attentionFrame` (`status.go:161,308,334`). | |
| 27 | - | - The client has no status-provider code today: no reference to | |
| 28 | - | `topRight`/`bottomRight` exists under `internal/usecase/client`. The client | |
| 29 | - | renders daemon-published frames verbatim. This contract does not change | |
| 30 | - | that until its implementation is approved. | |
| 31 | - | ||
| 32 | - | ## Opt-in client-local provider contract | |
| 33 | - | ||
| 34 | - | ### Configuration authority | |
| 35 | - | ||
| 36 | - | - The command string for a client-local provider comes exclusively from the | |
| 37 | - | **local** configuration authority (the config file loaded by the local | |
| 38 | - | process composition). A remote daemon MUST NOT supply a shell command over | |
| 39 | - | the wire; the wire carries only rendered text values (see fields below). | |
| 40 | - | - Existing `bar.top-right` / `bar.bottom-right` scripts remain daemon-host | |
| 41 | - | execution with unchanged semantics. Client-local providers target the same | |
| 42 | - | two slots but never replace daemon execution implicitly: both run, and | |
| 43 | - | precedence (below) decides what paints. | |
| 44 | - | - Opt-in per slot: unset means absent. No implicit fallback to a remote path, | |
| 45 | - | and remote pane CWD is never interpreted as a local path. A provider that | |
| 46 | - | needs a working directory uses only locally configured values. | |
| 47 | - | ||
| 48 | - | ### Generations | |
| 49 | - | ||
| 50 | - | - **Provider generation**: a monotonically increasing epoch owned by the | |
| 51 | - | executing side, bumped on every configuration reload affecting the slot. | |
| 52 | - | Stale values (older provider generation than the latest admitted for that | |
| 53 | - | slot and attachment) are discarded, never painted. | |
| 54 | - | - **Attachment generation**: the existing attachment identity/generation used | |
| 55 | - | in output arbitration binds a value to the attachment it was produced for. | |
| 56 | - | A value admitted for attachment A is never painted on attachment B; on | |
| 57 | - | navigation/switch, pending provider values for the retired attachment are | |
| 58 | - | dropped with the foreground owner. | |
| 59 | - | ||
| 60 | - | ### Bounds, queues, cancellation | |
| 61 | - | ||
| 62 | - | - Keep the existing daemon bounds unless separately approved: 1s timeout, | |
| 63 | - | 1024-byte stdout cap, 512-byte stderr cap, 256-rune display cap. | |
| 64 | - | Client-local execution adopts the same caps. | |
| 65 | - | - Bounded work queue per attachment (proposed depth 1 with coalescing, | |
| 66 | - | mirroring `running`/`pending`): a slow provider run never stacks; a newer | |
| 67 | - | tick marks pending and supersedes. | |
| 68 | - | - Cancellation: provider runs cancel on attachment retirement, slot | |
| 69 | - | reconfiguration, and client shutdown via the existing foreground-lifetime | |
| 70 | - | mechanism. A late completion after cancel is discarded. | |
| 71 | - | ||
| 72 | - | ### Sanitization and error presentation | |
| 73 | - | ||
| 74 | - | - All provider output passes daemon-side-equivalent sanitization on the | |
| 75 | - | executing side before admission: first line, valid UTF-8, ANSI/control | |
| 76 | - | stripping (`sanitizeBarScriptOutput` semantics), display-cap truncation. | |
| 77 | - | - Stale/error presentation is deterministic: on failure the slot keeps the | |
| 78 | - | last good value; a slot with no good value renders empty. Failures warn | |
| 79 | - | once per (provider, slot, signature) until the signature changes. No error | |
| 80 | - | text from stderr is painted (stderr is diagnostics only, capped). | |
| 81 | - | ||
| 82 | - | ### Precedence (same slot, daemon + client values) | |
| 83 | - | ||
| 84 | - | 1. A fresh admitted daemon value and a fresh admitted client value for the | |
| 85 | - | same slot: the **client-local value wins only when the slot is explicitly | |
| 86 | - | configured for client-local authority**; otherwise the daemon value wins. | |
| 87 | - | Authority is per slot, from local configuration, never negotiated. | |
| 88 | - | 2. A stale value (older generation) never overrides a fresh one regardless | |
| 89 | - | of side. | |
| 90 | - | 3. Absent/unset on the winning side falls through to the other side's latest | |
| 91 | - | admitted value, then to empty. | |
| 92 | - | ||
| 93 | - | ## Proposed fields (exact, pending approval) | |
| 94 | - | ||
| 95 | - | Configuration (local file, new keys under the existing parser): | |
| 96 | - | ||
| 97 | - | - `status.top-right.provider = local | daemon` (default `daemon`) | |
| 98 | - | - `status.top-right.command = "<shell>"` (default empty = absent) | |
| 99 | - | - `status.bottom-right.provider`, `status.bottom-right.command`: same shape. | |
| 100 | - | - `status.interval`: duration, default 5s, floor 1s (`MinBarInterval`); | |
| 101 | - | invalid values warn and keep prior, mirroring `bar.interval`. | |
| 102 | - | ||
| 103 | - | Wire/schema (rendered text only, never commands): | |
| 104 | - | ||
| 105 | - | - `StatusSlotValue{Slot: top-right | bottom-right, Text: string (already | |
| 106 | - | sanitized, ≤256 runes), ProviderGeneration: uint64, | |
| 107 | - | AttachmentID: <existing attachment identity type>}` carried inside the | |
| 108 | - | existing publication path for the owning attachment only. | |
| 109 | - | - No new carriage primitive: values travel with the attachment's ordered | |
| 110 | - | publication; no persistence-format change. | |
| 111 | - | ||
| 112 | - | Validation: unknown `provider` value warns and keeps `daemon`; empty command | |
| 113 | - | disables the slot; interval below minimum clamps and warns. | |
| 114 | - | ||
| 115 | - | ## Tests mapped (to be added only after approval) | |
| 116 | - | ||
| 117 | - | - `TestClientProviderCommandNeverComesFromWire` (sessionwire/client): | |
| 118 | - | crafted inbound value with command content is rejected; only text fields | |
| 119 | - | admitted. | |
| 120 | - | - `TestStaleProviderGenerationDiscarded` (client/composition): older | |
| 121 | - | provider generation never paints over a newer admitted value. | |
| 122 | - | - `TestProviderValueBoundToAttachment` (client): value admitted for A never | |
| 123 | - | paints on B; retirement drops pending values. | |
| 124 | - | - `TestProviderPrecedenceDaemonWinsByDefault` / `TestProviderPrecedenceLocalWinsWhenConfigured` | |
| 125 | - | (composition): per-slot authority matrix. | |
| 126 | - | - `TestProviderFailureKeepsLastGood` / `TestProviderEmptyWhenNoGood` | |
| 127 | - | (provider harness): deterministic stale/error presentation; warn-once. | |
| 128 | - | - `TestProviderQueueCoalesces` (provider harness, fake clock): slow run + | |
| 129 | - | newer tick yields at most one pending execution; cancel on retirement. | |
| 130 | - | - `TestRemoteCwdNeverInterpretedLocally` (provider harness): provider env | |
| 131 | - | contains no remote-derived path. | |
| 132 | - | - Existing pins stay green: `bar_refresh_test.go`, `bar_script` tests, | |
| 133 | - | `status` composition tests, config parse tests. | |
| 134 | - | ||
| 135 | - | ## Stop condition | |
| 136 | - | ||
| 137 | - | P4.2 ends at this document. No provider implementation, no new config key, | |
| 138 | - | no wire field, and no new dependency is authorized by this plan alone. | |
| @@ -1,138 +0,0 @@ | |||||
| 1 | - | # Client status contract (P4.2, design-only) | |||
| 2 | - | ||||
| 3 | - | Status: proposed. No production edits accompany this document. No provider | |||
| 4 | - | implementation is authorized by this plan alone; a runnable task breakdown | |||
| 5 | - | follows only after contract approval. | |||
| 6 | - | ||||
| 7 | - | ## Current daemon-host behavior (preserved) | |||
| 8 | - | ||||
| 9 | - | - Commands come only from the daemon-loaded config: `bar.top-right`, | |||
| 10 | - | `bar.bottom-right`, `bar.interval` (defaults empty/empty/5s, | |||
| 11 | - | `internal/adapters/config`, `domain.Defaults`). | |||
| 12 | - | - Execution is daemon-host via `barScriptRunner.run` | |||
| 13 | - | (`internal/usecase/daemon/bar_script.go`) with `ports.ShellCommandRunner`: | |||
| 14 | - | timeout 1s (`barScriptTimeout`), stdout cap 1024, stderr cap 512, display | |||
| 15 | - | cap 256, first-line + `stripANSIAndControls` sanitization, warn-once | |||
| 16 | - | failure dedup (`lastFailure`). | |||
| 17 | - | - Scheduling is per-session in `bar_refresh.go`: poller on the daemon clock | |||
| 18 | - | with `MinBarInterval` (1s) floor, `running`/`pending` coalescing, context | |||
| 19 | - | change (`lastContext`) forces refresh, `reload` channel wakes the poller on | |||
| 20 | - | config change, `refreshBarScriptsAllSessions` applies new commands | |||
| 21 | - | immediately. | |||
| 22 | - | - Scripts observe daemon-side context only: `VEV_ANCHOR`, `VEV_SESSION`, | |||
| 23 | - | `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, `VEV_COLS`. | |||
| 24 | - | - Composition reads snapshots at paint time: `barScriptSnapshot(sess)` feeds | |||
| 25 | - | `barState.topRight`/`bottomRight` alongside `status`, `statusFeedback`, | |||
| 26 | - | `mru`/`rankedRecent`, `attentionFrame` (`status.go:161,308,334`). | |||
| 27 | - | - The client has no status-provider code today: no reference to | |||
| 28 | - | `topRight`/`bottomRight` exists under `internal/usecase/client`. The client | |||
| 29 | - | renders daemon-published frames verbatim. This contract does not change | |||
| 30 | - | that until its implementation is approved. | |||
| 31 | - | ||||
| 32 | - | ## Opt-in client-local provider contract | |||
| 33 | - | ||||
| 34 | - | ### Configuration authority | |||
| 35 | - | ||||
| 36 | - | - The command string for a client-local provider comes exclusively from the | |||
| 37 | - | **local** configuration authority (the config file loaded by the local | |||
| 38 | - | process composition). A remote daemon MUST NOT supply a shell command over | |||
| 39 | - | the wire; the wire carries only rendered text values (see fields below). | |||
| 40 | - | - Existing `bar.top-right` / `bar.bottom-right` scripts remain daemon-host | |||
| 41 | - | execution with unchanged semantics. Client-local providers target the same | |||
| 42 | - | two slots but never replace daemon execution implicitly: both run, and | |||
| 43 | - | precedence (below) decides what paints. | |||
| 44 | - | - Opt-in per slot: unset means absent. No implicit fallback to a remote path, | |||
| 45 | - | and remote pane CWD is never interpreted as a local path. A provider that | |||
| 46 | - | needs a working directory uses only locally configured values. | |||
| 47 | - | ||||
| 48 | - | ### Generations | |||
| 49 | - | ||||
| 50 | - | - **Provider generation**: a monotonically increasing epoch owned by the | |||
| 51 | - | executing side, bumped on every configuration reload affecting the slot. | |||
| 52 | - | Stale values (older provider generation than the latest admitted for that | |||
| 53 | - | slot and attachment) are discarded, never painted. | |||
| 54 | - | - **Attachment generation**: the existing attachment identity/generation used | |||
| 55 | - | in output arbitration binds a value to the attachment it was produced for. | |||
| 56 | - | A value admitted for attachment A is never painted on attachment B; on | |||
| 57 | - | navigation/switch, pending provider values for the retired attachment are | |||
| 58 | - | dropped with the foreground owner. | |||
| 59 | - | ||||
| 60 | - | ### Bounds, queues, cancellation | |||
| 61 | - | ||||
| 62 | - | - Keep the existing daemon bounds unless separately approved: 1s timeout, | |||
| 63 | - | 1024-byte stdout cap, 512-byte stderr cap, 256-rune display cap. | |||
| 64 | - | Client-local execution adopts the same caps. | |||
| 65 | - | - Bounded work queue per attachment (proposed depth 1 with coalescing, | |||
| 66 | - | mirroring `running`/`pending`): a slow provider run never stacks; a newer | |||
| 67 | - | tick marks pending and supersedes. | |||
| 68 | - | - Cancellation: provider runs cancel on attachment retirement, slot | |||
| 69 | - | reconfiguration, and client shutdown via the existing foreground-lifetime | |||
| 70 | - | mechanism. A late completion after cancel is discarded. | |||
| 71 | - | ||||
| 72 | - | ### Sanitization and error presentation | |||
| 73 | - | ||||
| 74 | - | - All provider output passes daemon-side-equivalent sanitization on the | |||
| 75 | - | executing side before admission: first line, valid UTF-8, ANSI/control | |||
| 76 | - | stripping (`sanitizeBarScriptOutput` semantics), display-cap truncation. | |||
| 77 | - | - Stale/error presentation is deterministic: on failure the slot keeps the | |||
| 78 | - | last good value; a slot with no good value renders empty. Failures warn | |||
| 79 | - | once per (provider, slot, signature) until the signature changes. No error | |||
| 80 | - | text from stderr is painted (stderr is diagnostics only, capped). | |||
| 81 | - | ||||
| 82 | - | ### Precedence (same slot, daemon + client values) | |||
| 83 | - | ||||
| 84 | - | 1. A fresh admitted daemon value and a fresh admitted client value for the | |||
| 85 | - | same slot: the **client-local value wins only when the slot is explicitly | |||
| 86 | - | configured for client-local authority**; otherwise the daemon value wins. | |||
| 87 | - | Authority is per slot, from local configuration, never negotiated. | |||
| 88 | - | 2. A stale value (older generation) never overrides a fresh one regardless | |||
| 89 | - | of side. | |||
| 90 | - | 3. Absent/unset on the winning side falls through to the other side's latest | |||
| 91 | - | admitted value, then to empty. | |||
| 92 | - | ||||
| 93 | - | ## Proposed fields (exact, pending approval) | |||
| 94 | - | ||||
| 95 | - | Configuration (local file, new keys under the existing parser): | |||
| 96 | - | ||||
| 97 | - | - `status.top-right.provider = local | daemon` (default `daemon`) | |||
| 98 | - | - `status.top-right.command = "<shell>"` (default empty = absent) | |||
| 99 | - | - `status.bottom-right.provider`, `status.bottom-right.command`: same shape. | |||
| 100 | - | - `status.interval`: duration, default 5s, floor 1s (`MinBarInterval`); | |||
| 101 | - | invalid values warn and keep prior, mirroring `bar.interval`. | |||
| 102 | - | ||||
| 103 | - | Wire/schema (rendered text only, never commands): | |||
| 104 | - | ||||
| 105 | - | - `StatusSlotValue{Slot: top-right | bottom-right, Text: string (already | |||
| 106 | - | sanitized, ≤256 runes), ProviderGeneration: uint64, | |||
| 107 | - | AttachmentID: <existing attachment identity type>}` carried inside the | |||
| 108 | - | existing publication path for the owning attachment only. | |||
| 109 | - | - No new carriage primitive: values travel with the attachment's ordered | |||
| 110 | - | publication; no persistence-format change. | |||
| 111 | - | ||||
| 112 | - | Validation: unknown `provider` value warns and keeps `daemon`; empty command | |||
| 113 | - | disables the slot; interval below minimum clamps and warns. | |||
| 114 | - | ||||
| 115 | - | ## Tests mapped (to be added only after approval) | |||
| 116 | - | ||||
| 117 | - | - `TestClientProviderCommandNeverComesFromWire` (sessionwire/client): | |||
| 118 | - | crafted inbound value with command content is rejected; only text fields | |||
| 119 | - | admitted. | |||
| 120 | - | - `TestStaleProviderGenerationDiscarded` (client/composition): older | |||
| 121 | - | provider generation never paints over a newer admitted value. | |||
| 122 | - | - `TestProviderValueBoundToAttachment` (client): value admitted for A never | |||
| 123 | - | paints on B; retirement drops pending values. | |||
| 124 | - | - `TestProviderPrecedenceDaemonWinsByDefault` / `TestProviderPrecedenceLocalWinsWhenConfigured` | |||
| 125 | - | (composition): per-slot authority matrix. | |||
| 126 | - | - `TestProviderFailureKeepsLastGood` / `TestProviderEmptyWhenNoGood` | |||
| 127 | - | (provider harness): deterministic stale/error presentation; warn-once. | |||
| 128 | - | - `TestProviderQueueCoalesces` (provider harness, fake clock): slow run + | |||
| 129 | - | newer tick yields at most one pending execution; cancel on retirement. | |||
| 130 | - | - `TestRemoteCwdNeverInterpretedLocally` (provider harness): provider env | |||
| 131 | - | contains no remote-derived path. | |||
| 132 | - | - Existing pins stay green: `bar_refresh_test.go`, `bar_script` tests, | |||
| 133 | - | `status` composition tests, config parse tests. | |||
| 134 | - | ||||
| 135 | - | ## Stop condition | |||
| 136 | - | ||||
| 137 | - | P4.2 ends at this document. No provider implementation, no new config key, | |||
| 138 | - | no wire field, and no new dependency is authorized by this plan alone. | |||