Skip to content

Project titles: display name per project, accepted by -p (T459, #249) - #295

Merged
smarzban merged 4 commits into
mainfrom
feat/t459-project-titles
Nov 7, 2026
Merged

smarzban merged 4 commits into
mainfrom
feat/t459-project-titles

Conversation

@smarzban

@smarzban smarzban commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

Project titles (T459, #249): a project can carry a display name that replaces its folder name wherever tsk names it, and -p accepts it.

  • Board: ctrl+e on a Projects view row or a project picker row edits the title inline (single-line draft, prefilled with the title or folder name). Enter saves, Esc cancels, an empty draft clears. A taken title refuses on the status row (title Site A is used by <path>) and the editor stays open. The footer shows the full path while editing. Palette: set project title… on a project tab or a Projects view row. One ctrl+u undoes a title change.
  • CLI: tsk project title <name> <title> and tsk project title <name> --clear, with codes unknown-project, title-taken, invalid-title. -p, !p, and the project verbs match a title (case-insensitive) before a basename. Ambiguity refusals list a titled candidate as Title (path).
  • Display: title replaces the folder name on the project tab, picker, Projects view and rail, row labels, task page scope, launch card, archived project X messages, and tsk list text (--group project, --all scope headings, --sort project). Sorting by name uses the displayed name. Footers and JSON project keep the full path; JSON rows (list, list T12, add) gain project_title, omitted when the project has none.
  • Store v16 → v17: ProjectRecord gains title: Option<String> (skipped when none). No-op migration step plus current_store_v17.json. A record lives while archived or titled; unarchive keeps a title, clearing a title keeps an archive. Concurrent saves merge archived and title per field (ProjectIntent), last writer wins per field, never dropping the other. New UndoEntry::ProjectTitle { path, previous, title }; a stale one refuses with StaleProjectUndo and is pruned at save.

Docs: board.md (Project titles section, palette row), keys.md, cli.md (scope, project title, JSON fields, refusal table), capture.md (!p), storage.md (format 17), CONTEXT.md, llms.txt, CHANGELOG (Added, credit #249; the unreleased store-16 Breaking line now says 17), What's new notice id 7 (unreleased) gains a paragraph, tsk-cli skill wording (version stays 1.7.0, unreleased since v0.13.0 shipped 1.6.0; hash pin refreshed).

Decisions and deviations

Decisions 1–9 of the brief are implemented as written, except where noted:

  • Title validation beyond the brief: a title refuses / (and \ on Windows: the platform path separator, so -p can always reach a title), a control character, desk/global (they are scope labels in --group project and !p), and more than 80 characters. Whitespace is trimmed and inner runs collapse to one space.
  • Unchanged folder name: Enter on an untitled project's unchanged prefill saves nothing (otherwise every accidental ctrl+e, Enter would write a title equal to the folder name).
  • Taskless project: the Projects view's here row before its first task refuses at ctrl+e (<name> has no tasks yet: add one, then title it). Found in the PTY smoke; the store only keeps titles for projects it knows, matching how archive refuses (nothing to archive in X).
  • Undo in the picker: the picker's main tab ctrl+u was inert; it now undoes the top entry when that entry is a title change, so a title set in the picker undoes there. Every other undo still waits for the board.
  • Palette route: on a project tab, set project title… opens the picker on that project and edits its row there; the picker closes after the save (and on Esc).
  • Undo never makes a duplicate: a title entry is stale when its project no longer carries the title it set, or when the title it would restore now belongs to another project.
  • Refusal codes: project title refusals carry codes (per brief item 6); project archive|unarchive keep their message-only refusals. cli.md and the skill say so.
  • Clicks while editing are inert, so a stray click never drops a draft.
  • Known edge, handled at resolution: two processes titling different projects with the same title in overlapping saves can both land (uniqueness is checked against each writer's merged copy). Every resolver (-p, !p, project verbs) collects all title matches and refuses two or more as ambiguous, listing Title (path) for each, so it never picks one silently. Renaming either project clears it.
  • Concurrency of uniqueness vs merge is otherwise last-writer-wins per field, as decision 2.

Seed script and real run

Isolated state only (TSK_STATE_DIR and --state-dir under a fresh /tmp/tsk-459-*). The seed binary was the maintainer's current main build (store v16, tsk 0.13.0), copied to a scratch dir, not a tagged release.

#!/bin/sh
# Usage: seed.sh <old-tsk> <new-tsk>
set -eu
OLD="$1"; NEW="$2"
ROOT=$(mktemp -d /tmp/tsk-459-XXXXXX); STATE="$ROOT/state"
mkdir -p "$STATE" "$ROOT/site-a/wordpress/wp-content" "$ROOT/site-b/wordpress/wp-content"
A="$ROOT/site-a/wordpress/wp-content"; B="$ROOT/site-b/wordpress/wp-content"
export TSK_STATE_DIR="$STATE" TSK_NO_UPDATE_CHECK=1
"$OLD" add -t "Fix checkout CSS" -p "$A" --state-dir "$STATE"
"$OLD" add -t "Update plugins" -p "$B" --state-dir "$STATE"
"$OLD" add -t "Desk note" --desk --state-dir "$STATE"
"$NEW" list -p wp-content --state-dir "$STATE" || echo "exit $?"
"$NEW" list --all --group project --state-dir "$STATE"
"$NEW" project title "$A" "Site A" --state-dir "$STATE"
"$NEW" project title "$B" "site a" --state-dir "$STATE" || echo "exit $?"
"$NEW" project title "$B" "Site B" --state-dir "$STATE"
"$NEW" list -p "site a" --state-dir "$STATE"
"$NEW" list --all --group project --state-dir "$STATE"
"$NEW" list -p "Site B" --json --state-dir "$STATE"
"$NEW" list -p wp-content --state-dir "$STATE" || echo "exit $?"
"$NEW" project title "Site B" --clear --state-dir "$STATE"
"$OLD" list --all --state-dir "$STATE" || echo "exit $?"

Before (v16 store, seeded by the previous build):

format_version: 16
tsk list: unknown-project: project wp-content is ambiguous: /tmp/tsk-459-BuRjmo/site-a/wordpress/wp-content, /tmp/tsk-459-BuRjmo/site-b/wordpress/wp-content
exit 1
OPEN
  desk
    - 3 Desk note
  site-b/wordpress/wp-content
    - 2 Update plugins
  site-a/wordpress/wp-content
    - 1 Fix checkout CSS

After (new build; first save migrates and keeps tsk.json.v16):

titled project Site A (/tmp/tsk-459-BuRjmo/site-a/wordpress/wp-content)
tsk project: title-taken: title site a is used by /tmp/tsk-459-BuRjmo/site-a/wordpress/wp-content
exit 1
titled project Site B (/tmp/tsk-459-BuRjmo/site-b/wordpress/wp-content)
format_version: 17
tsk.json.v16
{
 "/tmp/tsk-459-BuRjmo/site-a/wordpress/wp-content": { "archived": false, "title": "Site A" },
 "/tmp/tsk-459-BuRjmo/site-b/wordpress/wp-content": { "archived": false, "title": "Site B" }
}
$ tsk list -p "site a"
OPEN
 - 1 Fix checkout CSS
$ tsk list --all --group project
OPEN
  desk
    - 3 Desk note
  Site B
    - 2 Update plugins
  Site A
    - 1 Fix checkout CSS
$ tsk list -p "Site B" --json
[{"after":[],"assignee":null,"assignee_kind":null,"base":null,"before":[],"id":"5ea02f9a-…","number":2,"project":"/tmp/tsk-459-BuRjmo/site-b/wordpress/wp-content","project_title":"Site B","status":"open","thread":null,"title":"Update plugins"}]
$ tsk list -p wp-content
tsk list: unknown-project: project wp-content is ambiguous: Site A (/tmp/…/site-a/wordpress/wp-content), Site B (/tmp/…/site-b/wordpress/wp-content)
$ tsk project title "Site B" --clear
cleared title of project wp-content (/tmp/tsk-459-BuRjmo/site-b/wordpress/wp-content)
$ <previous build> list --all
tsk list: store format 17 is unsupported by this tsk (expected 16)
exit 3

Board, driven in a PTY (110×30, pyte; HERDR_ENV unset)

3 → Projects view, ctrl+e on the taskless here row refuses; j, ctrl+e, ctrl+u, Site A, Enter saves; p, move to the other wp-content, ctrl+e, site a, Enter refuses with the editor open:

 ▸ tsk-459-pty-… · here                                         ·          ·        ·     ·
 tsk-459-pty-… has no tasks yet: add one, then title it                        → project pane

 ▸ Site A                ·      ·    1    · │        (editing; footer: /tmp/…/site-a/wp-content)
 enter save title · esc cancel

                        │   desk                                                     │
                        │   tsk-459-pty-…                                            │
                        │   Site A                                                   │
                        │ ▸ site a                                                   │
                        ├────────────────────────────────────────────────────────────┤
                        │ enter save title · esc cancel                              │
 title site a is used by /tmp/tsk-459-pty-…/site-a/wp-content                  → project pane

Afterwards tsk list --all --group project shows Site A and wp-content.

Tests

New:

  • domain::task::tests::project_titles_are_unique_ignoring_case_and_refuse_desk_slashes_and_controls
  • domain::task::tests::a_project_record_lives_while_archived_or_titled
  • domain::task::tests::a_title_change_undoes_once_and_a_stale_one_refuses (incl. an undo that would recreate a duplicate)
  • scope::project_title_tests::permissive_resolution_takes_a_title_first_and_labels_ambiguity (also: a title two projects share refuses in both resolvers)
  • cli_archive::project_title_reaches_add_json_scope_headings_and_project_sort (add --json created and existing, --all headings, --sort project)
  • board_project_title::the_picker_and_the_projects_view_sort_by_the_title
  • scope_resolver::project_titles_resolve_first_and_label_ambiguity
  • store_persist::literal_v17_fixture_is_current_and_round_trips, literal_v16_fixture_migrates_to_v17_and_keeps_its_archived_project, project_titles_round_trip_through_the_store, project_title_and_archive_from_two_writers_merge_field_by_field
  • cli_archive::project_title_sets_resolves_lists_refuses_a_duplicate_and_clears, project_title_usage_errors_exit_2_and_an_unknown_project_refuses
  • board_project_title::* (6): Projects view edit + duplicate + Esc + undo; unchanged folder name / empty clears; picker edit + ctrl+u in the picker; palette route; taskless here refusal; a failed save keeps the editor and draft through Cancel
  • board_project_title::the_projects_view_footer_offers_ctrl_e_title_and_a_click_opens_the_editor
  • Footers: the project picker (both tabs) reads ctrl+e title beside ctrl+f archive (shed 2: at 80 columns ↑↓ move sheds first; on the compact card it sheds with the sort). The Projects view verb row reads enter open · / search · ctrl+e title · : commands · ? help, and a click on it opens the editor.
  • Goldens (regenerated, not hand-edited): new projects_index_titled_110x30, projects_index_title_edit_110x30, project_picker_title_edit_78x24, project_picker_110x30, project_picker_40x24; projects_index_110x30 and projects_preview_split_110x30 gain the ctrl+e title verb.

Updated: store version expectations (16 → 17), palette catalog lists (set project title…), docs parity (new palette condition), queue/render call sites for the ProjectNames parameter.

Revert-checked (fix hunk reverted by hand, test failed, fix restored): field-by-field merge of the title intent; the editor outliving the save; title-first resolution; the no-duplicate undo guard; refusing a shared title in resolution; the palette command's intent (the palette test drives : + query + Enter through resolve_board_command); sorting by the shown name; project_title in add JSON; titled --all headings.

Verification

  • Green bar: cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test: pass.
  • cd site && npm ci && npm test: 63/63 pass.
  • cargo build --release: ok; real run and PTY board smoke above.

Not run

  • Live Herdr smoke (HERDR_ENV unset); the board was driven in a PTY instead. Narrow-width picker editing was only covered by the 78×24 golden.
  • HOME was not overridden for the real run (the harness refuses env injection of HOME); isolation was TSK_STATE_DIR plus --state-dir, and only CLI verbs and the board ran, none of which read HOME for state.
  • Windows: no Windows build or clippy (x86_64-pc-windows-gnu) was run; CI covers it. On Windows \ is also refused in a title.
  • npm run test:parity passed (80/80). The web demo cannot edit titles, so its Projects view verb row was left without the hint; parity does not compare that row. The parity PTY smoke was not run.
  • No independent review yet.

Refs #249

… (T459)

ctrl+e on a Projects view or project picker row edits a project's title
inline; the palette's set project title… does it from a project tab.
tsk project title <name> <title> | --clear sets it from the CLI. A title
replaces the folder name wherever tsk names the project; footers and
--json keep the full path, and JSON rows gain project_title. Titles are
unique ignoring case, resolve before basenames for -p, !p and the project
verbs, and undo as one ctrl+u entry. Store v16 -> v17 adds the optional
record title; a record lives while archived or titled, and concurrent
saves merge title and archive field by field. Refs #249.
@vercel

vercel Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
herdr-tsk Ignored Ignored Preview Nov 7, 2026 3:30pm UTC

…adings, sort, palette; docs list every title rule (T459 review)
@smarzban
smarzban merged commit 36610e8 into main Nov 7, 2026
10 checks passed
@smarzban
smarzban deleted the feat/t459-project-titles branch November 7, 2026 20:39
cursor Bot pushed a commit that referenced this pull request Nov 8, 2026
#295 left the picker rustdoc and a stranded clippy allow above
PROJECT_TITLE_EDIT_FOOTER. Put the three-line doc back on the fn and
keep the footer's one-line const doc.

Co-authored-by: Saeed <smarzban@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant