1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
#[allow(clippy::wildcard_imports)]
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mode {
Board,
/// The batch modal (WP07). The modal's state is in [`App::bulk`]; the mode says
/// only that it is open, exactly as `Mode::FilterModal` does.
BulkModal,
/// The triage inbox (WP06, T029). A modal over the board like settings and
/// the batch modal, so the board, its tabs and its card cursor are all
/// still there underneath and unchanged when it closes.
///
/// The inbox's own cursor and selection live in [`App::triage`] rather than
/// in [`BoardState`], which is what makes "the normal board/detail
/// selection is unchanged when the inbox is not active" a property of
/// which struct each cursor is in rather than something a test has to
/// remember to restore.
TriageInbox,
/// The F3 Cycle mode (tui-cycle-mode): cycles grouped current / future /
/// archived with statistics for the selected one. Like the triage inbox it
/// is the mode's content, not a modal over the board; the
/// planning-management surface is reached from it with `m`.
Cycle,
/// The all-issues mentions view (OpenSpec change `issue-mentions`): every
/// pending `@mention` the local user holds, across issues. A modal over the
/// board like the batch modal, so the cards stay on screen underneath.
///
/// Its state is [`crate::ui::mentions::MentionsView`], held as
/// [`App::mentions_view`] — `Some` exactly while it is open, so a closed view
/// has no rows to act on.
Mentions,
/// The per-issue mention notice (OpenSpec change `issue-mentions`): the issue
/// just opened carries unread mentions for the local user.
///
/// Deliberately **non-mutating**: dismissing it (Esc, or `i` for "ignore")
/// writes nothing, which is what makes the mention reappear the next time the
/// issue is opened. Only "mark read" reaches the store. Its keys are raw (see
/// `handle_key`), for the reason the recurring warning's are: the board binds
/// some of the same letters and the binding layer would rewrite them first.
MentionNotice,
/// The recurring-issue due warning (mission `recurring-issues-01M3C7G7`,
/// WP07): a non-mutating overlay over the board, `Some` exactly while it is
/// open. Its state is [`crate::ui::recurring_warning::RecurringWarning`].
///
/// The rows come from the shared detector, and each of the four answers is an
/// explicit press — the overlay itself never generates, defers or ignores
/// anything, which is what makes "detection is read-only" true in the TUI.
/// Its keys are raw (see `handle_key`): the board's `d`/`g` bindings would
/// rewrite them into `Esc`.
RecurringWarning,
/// The WIP override confirmation (mission `configurable-wip-limits-01M3C6WX`,
/// WP05 T026): a blocking limit refused a move and the user is supplying the
/// reason that authorizes it. An overlay over the board, like the due warning;
/// it carries the one free-text field, so the reason is typed rather than
/// pressed.
WipOverride {
/// The reason being typed.
buffer: String,
},
/// The F7 Stats page's **Health tab** (mission
/// `project-issue-health-status-01M3C8HX`, WP06): one row per scope, with its
/// state, layer and derived value, and the diagnostics below. Read-only — the
/// mutations live in the detail view's form.
Stats,
/// The health form (mission `project-issue-health-status-01M3C8HX`, WP06 T032):
/// the audit fields a mutation needs, opened from the detail pane. An overlay
/// over the pane, like the WIP override: it carries free text, so `Tab` walks the
/// fields and `Enter` submits.
HealthForm,
/// The time-entry form (mission `per-issue-time-tracking-01M3C9A4`, WP05 T025):
/// the audit fields an add, a correction or a deletion needs, opened from the `F7`
/// Stats page's Time tab. An overlay over the page, like the health form: it
/// carries free text, so `Tab` walks the fields and `Enter` submits.
///
/// The form's own state is [`App::time_form`], and the tab underneath keeps
/// drawing, so the entry rows the correction is about stay on screen while its
/// fields are typed.
TimeForm,
/// The Time tab's filter editor (WP05 T026): the issue, assignee and period
/// criteria the shared `TimeFilter` is built from. A separate mode only because it
/// is a separate field set — the keys (`Tab`/`Enter`/`Esc`) are the time form's.
TimeFilterForm,
/// The F8 Schedule page (tix #1593): the recurring series and their state on its
/// **Series** tab, and the configured cycle schedules with the cycles they
/// generated on its **Cycle schedules** tab (mission
/// `repeating-cycle-schedules-01M3C7W1`, tix #181), each with the keys that act
/// on them. A top-level mode like `F4`/`F5`/`F6`.
Schedule,
/// The F6 Repos mode (tix #1529): the child (and parent) repositories this
/// project coordinates. `j`/`k` walk the rows and `Enter` runs `tix` inside
/// the selected repository. With none configured, the body explains how to
/// add one under `[repos]`.
Repos,
/// The F11 Library page (tix #1593): the saved views and the issue/form
/// templates that already exist, listed with the selected record's detail.
/// `Tab` switches registries, `j`/`k` walk rows, and `Enter` applies the
/// selected view to the board. Read-only apart from that apply — see
/// [`crate::ui::library`].
Library,
/// The F10 **Portfolio** page (WP06): the project registry this mission
/// introduces, listed with each project's derived member count and the selected
/// project's record and members beside it. Read-only — see
/// [`crate::ui::portfolio`].
Portfolio,
RolloverDialog,
Detail,
Form,
ConfirmDelete,
/// Archive or restore one issue, after a `y`/`N` confirmation.
ConfirmArchive {
filename: String,
restore: bool,
},
MoveIntoDone {
buffer: String,
},
/// The blocked-move acknowledgement prompt (WP04, FR-004).
///
/// Reached instead of applying a move toward the board's completion column
/// when the issue has open blockers. `warning` is the **shared** text from
/// `commands::blocked_note`, so the TUI says exactly what the CLI says.
ConfirmBlockedMove {
warning: String,
},
/// The reasoned-override prompt (WP05, FR-002, contract §3.3).
///
/// Reached instead of applying a move that leaves a **terminal** category
/// when the board declares a `category_map`. `reason` is the **shared**
/// diagnostic from the service, so the TUI says exactly what the CLI says —
/// parity includes the message, not only the accept/refuse decision.
///
/// This prompt is the whole reason WP05 exists on this path. Until it, a
/// terminal-exit transition was refused by the seam with a message naming
/// `tix move … --reason`, and the TUI had **no way to supply one** — the flag
/// is a CLI spelling and the TUI has no flags. So a user who moved an issue to
/// `done` in the TUI could not move it back, in the TUI, at all.
TransitionReason {
/// The refusal being overridden, rendered verbatim.
reason: String,
/// What the user has typed so far.
buffer: String,
},
Help {
return_to: Box<Mode>,
},
/// The startup tips modal (tix #1632): one tip from the embedded
/// [`crate::tips`] list, over whatever the session opened on. `return_to` is
/// the mode it was opened from (the board, or the create-issue form `tix new`
/// opens), so closing it lands exactly where the session was headed.
///
/// Its own state is [`App::tips`], `Some` exactly while it is open. Its keys
/// are raw (see `handle_key`): the board binds some of the same letters, and
/// the binding layer would rewrite them first.
Tips {
return_to: Box<Mode>,
},
/// The issue action menu (`Enter`, a plain left click, or right-click);
/// `return_to` is the mode it was opened from. Activation runs the exact
/// action the equivalent key binding would.
ActionMenu {
return_to: Box<Mode>,
},
Filter {
buffer: String,
},
FilterModal,
/// The saved-views modal (WP05, T024–T027). A modal over the board like
/// settings and the batch modal, so the board, its tabs, and its card cursor
/// are all still there underneath and unchanged when it closes.
///
/// Its own state lives in [`App::views`] rather than in `BoardState`, so
/// "navigation is unchanged when the view list is not open" is a property of
/// which struct holds the cursor rather than something a test remembers to
/// restore.
Views,
/// The F12 Config mode (tui-config-mode): the sectioned surface over
/// dashboards, cycles, and milestones. Like the triage inbox and the cycle
/// listing it is the mode's content, not a modal over the board. Its state
/// is [`App::settings`], and its cycles/milestones sections drive
/// [`App::planning`] too.
Settings,
PlanningManagement,
PlanningForm,
/// Calendar date picker popup; `return_to` is the modal it overlays.
DatePicker {
return_to: Box<Mode>,
},
Comment {
buffer: String,
},
Asset {
buffer: String,
},
/// The editor round trip left a file with a problem (tix #1506).
///
/// One prompt for every post-edit problem: a file that will not parse, a
/// file `tix lint` is unhappy about, or an edit that lost its title. It
/// lists each problem with its diagnostic and a proposal, and offers the fix
/// tix can apply, the editor again on the retained copy, an explicit
/// declaration of an undeclared key, acknowledgement, and a discard that
/// needs a second confirmation.
///
/// Nothing is discarded automatically: the retained copy (`temp`) survives
/// acknowledgement and is only removed by the confirmed discard. When
/// `written` is true the edited content is already on disk (`tix lint`
/// findings arrive after the write, tix #1510's #1506 behaviour), so the
/// prompt is a report; when it is false nothing has been written and the fix
/// writes the repaired file for the first time.
///
/// `key` is the undeclared top-level frontmatter key the prompt can declare,
/// `Some` only for an `unknown_key` problem with a non-empty value. It is what
/// `confirm`/`declare_key` write into `tix.toml`; every other problem leaves
/// it `None`, so those chords say there is nothing to declare.
EditRepair {
filename: String,
/// `kind: message` of the first problem, for the status line and the
/// prompt's heading.
message: String,
key: Option<String>,
/// Every classified problem, in the order the diagnostics arrived.
problems: Vec<EditProblem>,
/// The retained edited copy — the editor's temp file when one exists.
/// The only thing a confirmed discard removes.
temp: Option<PathBuf>,
/// Whether the edited content is already on disk.
written: bool,
/// A discard has been asked for once; the second keypress is the
/// decision. `Esc` never sets it.
discard_armed: bool,
},
/// The issue file changed on disk while the editor had it open (tix #1510).
///
/// Entered **instead of** the write, so nothing about the raced edit is on
/// disk: the newer file is untouched and the editor's version is still the
/// temp copy. The three keys the prompt names are the only way out, and two of
/// them — `a`/`Esc` (keep the on-disk version) and `c` (compare in the
/// editor) — never write the editor's version at all. Only `o`/`y` overrides.
EditConflict {
/// The issue file, relative to the store's `issues_dir`.
filename: String,
/// The edited temp copy — the editor's version, kept until the user
/// decides (abandoning removes it, overriding writes it, comparing
/// reopens it).
temp: PathBuf,
/// The newer on-disk file, shown so the user can open it by hand.
disk_path: PathBuf,
},
/// Create a relationship from the detail view (#1359), in two steps.
///
/// `Step::Target` asks for the other issue as a **selector** — the same string
/// `tix link <from> <to>` takes, so `#4`, `4`, or a title fragment all work and
/// an unresolvable one produces the CLI's own error rather than a TUI-specific
/// one. `Step::Kind` then asks for the kind, and it is **required**: C-001 forbids
/// an untypeable relationship, so there is no default to fall back to and an
/// empty or unrecognised answer is a refusal, not a guess.
LinkCreate {
buffer: String,
step: LinkStep,
/// The target resolved by the `Target` step, carried forward to `Kind`.
///
/// Carried rather than re-resolved: the first step already applied the CLI's
/// selector rules and reported its errors, and re-reading the buffer's
/// meaning a second time would be a second place for the two to disagree. A
/// `Kind` step with no target cannot happen — the mode is only entered in
/// that state with one.
target: Option<u32>,
},
/// Re-kind or retarget the relationship under the detail pane's cursor.
///
/// Carries the target it was opened with so the kind step can submit an edit
/// rather than a create: `tix link edit <id>` is an edit precisely because the
/// stable id survives a re-kind, and a create-then-remove pair would lose it.
LinkEdit {
index: usize,
buffer: String,
step: LinkStep,
/// The new target, when the user supplied one. `None` means **keep the
/// existing target** and change only the kind — the same as `tix link edit
/// <id> --type …` with no `--to`.
target: Option<u32>,
/// The new kind, when the user supplied one. `None` likewise means keep.
kind: Option<crate::relationships::RelationKind>,
},
/// Confirm removing the relationship under the cursor.
///
/// A relationship is stored in exactly one issue's file, so removing it is a
/// write to another issue from the one on screen. It gets a confirmation for the
/// same reason `ConfirmDelete` does, and the confirmation names the edge.
ConfirmRemoveRelationship {
index: usize,
},
/// Set, change or clear the open issue's parent (tix #1384).
///
/// The detail pane's `P` opens it. Enter submits the typed selector and Esc
/// cancels; an **empty** line clears the parent. The write goes through
/// `Store::set_parent`/`Store::clear_parent`, the same hierarchy seam
/// `tix edit --parent` uses, so one accepted change appends exactly one
/// `parent_set`/`parent_changed`/`parent_cleared` History event — the TUI
/// half of the CLI/TUI parity the create-only form could not express.
ParentPrompt {
buffer: String,
},
}