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:
@@ -151,7 +151,8 @@ public class NovelAgentToolset(
|
||||
JsonInput.String(input, "arc_summary"),
|
||||
JsonInput.String(input, "voice"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Strings(input, "tags")), ct), c => c.ToResponse(), "Project", projectId));
|
||||
JsonInput.Strings(input, "tags"),
|
||||
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Project", projectId));
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_character",
|
||||
@@ -181,7 +182,42 @@ public class NovelAgentToolset(
|
||||
JsonInput.String(input, "arc_summary"),
|
||||
JsonInput.String(input, "voice"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Strings(input, "tags")), ct), c => c.ToResponse(), "Character", characterId);
|
||||
JsonInput.Strings(input, "tags"),
|
||||
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Character", characterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"link_character_identity",
|
||||
"Record that a character is really another character — e.g. one introduced under one name "
|
||||
+ "who is later revealed to be a character already in the project under another name. Both "
|
||||
+ "keep their own dossier and beats; the canonical identity is whichever character you link to.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("character_id", "Id of the character being revealed as someone else.", required: true)
|
||||
.Str("same_character_as_id", "Id of the character this one really is.", required: true)
|
||||
.Str("revealed_in_chapter_id", "Id of the chapter where the reveal happens, if any.")
|
||||
.Str("note", "Context on the reveal, e.g. how and why the disguise held.")
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
return await OrNotFound(characters.LinkIdentityAsync(
|
||||
characterId,
|
||||
new LinkCharacterIdentityRequest(
|
||||
JsonInput.RequiredGuid(input, "same_character_as_id"),
|
||||
JsonInput.Guid(input, "revealed_in_chapter_id"),
|
||||
JsonInput.String(input, "note")), ct), c => c.ToResponse(), "Character", characterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"unlink_character_identity",
|
||||
"Remove a character's identity link, restoring it to its own separate identity.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("character_id", "Id of the character to unlink.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
return await DeletedOrNotFound(characters.UnlinkIdentityAsync(characterId, ct), "Character", characterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
@@ -289,6 +325,27 @@ public class NovelAgentToolset(
|
||||
.Where(g => g != Guid.Empty)]), ct), list => list.Select(b => b.ToResponse()), "Chapter", chapterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"move_beats",
|
||||
"Move one or more beats from one chapter to another, appending them to the target "
|
||||
+ "chapter's end in the order given.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Id of the beats' current chapter.", required: true)
|
||||
.Str("target_chapter_id", "Id of the chapter to move the beats into.", required: true)
|
||||
.StringArray("beat_ids", "Ids of the beats to move.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
|
||||
return await OrNotFound(beats.MoveAsync(
|
||||
chapterId,
|
||||
new MoveBeatsRequest(
|
||||
JsonInput.RequiredGuid(input, "target_chapter_id"),
|
||||
[.. (JsonInput.Strings(input, "beat_ids") ?? [])
|
||||
.Select(id => Guid.TryParse(id, out var g) ? g : Guid.Empty)
|
||||
.Where(g => g != Guid.Empty)]), ct), list => list.Select(b => b.ToResponse()), "Chapter", chapterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"list_tags",
|
||||
"List the project's tags with how many characters, chapters and beats carry each. "
|
||||
@@ -596,7 +653,8 @@ public class NovelAgentToolset(
|
||||
.Str("arc_summary", "How they change over the course of the book.")
|
||||
.Str("voice", "Speech patterns and register that make their dialogue theirs.")
|
||||
.Str("notes", "Anything else worth recording.")
|
||||
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.");
|
||||
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.")
|
||||
.StringArray("aliases", "Other names this character is known by. Replaces the existing aliases.");
|
||||
}
|
||||
|
||||
private static JsonSchemaBuilder BeatSchema() =>
|
||||
|
||||
Reference in New Issue
Block a user