Add character aliases/identity links, move-beats, keyboard help overlay

- Characters can carry aliases and be linked as the same underlying
  person (canonical SameCharacterAsId, optional reveal chapter/note),
  surfaced through the API, MCP tools, agent toolset, and web UI.
- Characters page redesigned as a filterable/sortable table (name+
  aliases, role, importance, occupation, tags) instead of a sidebar
  list, to stay usable as the cast grows.
- Beats can be moved between chapters (BeatService.MoveAsync + MCP/
  agent tool + endpoint).
- Add a keyboard-shortcuts help overlay (HelpButton/HelpOverlayContext)
  wired into the project layout.
- CLAUDE.md: require every frontend component to carry a unique id
  attribute; apply it to CharacterMultiSelect and MarkdownEditor.
This commit is contained in:
James Wampler
2026-08-17 17:26:50 -07:00
parent b4f4b35e3c
commit f124b9b4bb
32 changed files with 3002 additions and 356 deletions
+31 -4
View File
@@ -48,7 +48,8 @@ public static class CharacterTools
[Description("How they change over the course of the book.")] string? arcSummary = null,
[Description("Speech patterns and register that make their dialogue theirs.")] string? voice = null,
[Description("Anything else worth recording.")] string? notes = null,
[Description("Tags for cross-referencing. Unknown tags are created.")] string[]? tags = null) =>
[Description("Tags for cross-referencing. Unknown tags are created.")] string[]? tags = null,
[Description("Other names this character is known by.")] string[]? aliases = null) =>
api.PostAsync($"/api/projects/{projectId}/characters", new
{
name,
@@ -67,7 +68,8 @@ public static class CharacterTools
arcSummary,
voice,
notes,
tags
tags,
aliases
}, ct);
[McpServerTool(Name = "update_character")]
@@ -94,7 +96,8 @@ public static class CharacterTools
[Description("How they change over the course of the book.")] string? arcSummary = null,
[Description("Speech patterns and register.")] string? voice = null,
[Description("Anything else worth recording.")] string? notes = null,
[Description("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null) =>
[Description("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null,
[Description("Other names this character is known by. Replaces the existing aliases.")] string[]? aliases = null) =>
api.PatchAsync($"/api/characters/{characterId}", new
{
name,
@@ -113,7 +116,8 @@ public static class CharacterTools
arcSummary,
voice,
notes,
tags
tags,
aliases
}, ct);
[McpServerTool(Name = "get_character_beats")]
@@ -191,4 +195,27 @@ public static class CharacterTools
[Description("What the relationship is like, and where it is headed.")] string? description = null) =>
api.PostAsync($"/api/characters/{characterId}/relationships",
new { relatedCharacterId, relationshipType, description }, ct);
[McpServerTool(Name = "link_character_identity")]
[Description("Record that this character is really another character — e.g. a character introduced "
+ "under one name who is later revealed to be a character already in the project under "
+ "another name. Both characters keep their own dossier and beats; the canonical identity "
+ "is whichever character you link to.")]
public static Task<CallToolResult> LinkCharacterIdentity(
NovelApiClient api,
[Description("Id of the character being revealed as someone else.")] Guid characterId,
[Description("Id of the character this one really is.")] Guid sameCharacterAsId,
CancellationToken ct,
[Description("Id of the chapter where the reveal happens, if any.")] Guid? revealedInChapterId = null,
[Description("Context on the reveal, e.g. how and why the disguise held.")] string? note = null) =>
api.PutAsync($"/api/characters/{characterId}/identity",
new { sameCharacterAsId, revealedInChapterId, note }, ct);
[McpServerTool(Name = "unlink_character_identity")]
[Description("Remove a character's identity link, restoring it to its own separate identity.")]
public static Task<CallToolResult> UnlinkCharacterIdentity(
NovelApiClient api,
[Description("The character's id.")] Guid characterId,
CancellationToken ct) =>
api.DeleteAsync($"/api/characters/{characterId}/identity", ct);
}