-
-
Notifications
You must be signed in to change notification settings - Fork 499
agent command — hierarchical team grouping, resumable session IDs, AI titles
#895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
thomasttvo
wants to merge
28
commits into
ryoppippi:main
Choose a base branch
from
thomasttvo:feat/agent-tracking
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
0a5b2dc
fix(ccusage): fix 4 agent command bugs
f08ba8d
fix(ccusage): wrap agent names at / boundary and prioritize compactio…
86cfe28
fix(ccusage): extract compaction titles from isCompactSummary entries
037b238
feat(ccusage): add find-agent-id script for recovering past agent res…
7d1a006
feat(ccusage): generate AI titles for sessions without compaction sum…
925cffc
refactor(ccusage): remove slug fallback, use truncated sessionId instead
5adbe50
feat(ccusage): group team members under their lead session in agent o…
8c73db1
fix(ccusage): handle array content and isMeta in user message extraction
4d28ef2
feat(ccusage): add % column and group all team members under headers
7d4a40b
feat(ccusage): discover orphan team→lead mappings for proper nesting
49b6b96
feat(ccusage): show truncated session ID on lead rows for cross-refer…
f5336f3
fix(ccusage): use full session UUID instead of 8-char prefix on lead …
e420bc2
fix(ccusage): widen Agent column to fit full 36-char session UUID
31228cd
refactor(ccusage): remove AI title generation, use compaction summari…
a275023
Revert "refactor(ccusage): remove AI title generation, use compaction…
9caecb6
fix(ccusage): improve AI title quality by filtering trivial messages
084a984
fix(ccusage): remove trailing · when session has no title
0a9b325
fix(ccusage): add ellipsis on truncated titles, bump cache to v12
ba842f3
fix(ccusage): fix title placeholders, trailing separators, and duplic…
58a6bee
fix(ccusage): show UUIDs on titled rows, parse teammate-message conte…
294ca4e
fix(ccusage): improve AI title generation for all sessions
5121f2d
fix(ccusage): strict AI titles, command-args extraction, compaction v…
31769cb
fix(ccusage): address CodeRabbit review feedback on agent command
422493b
fix(ccusage): enforce noun-phrase AI titles with few-shot examples
a1c57f0
fix(ccusage): skip bare UUIDs when extracting title from user messages
11bceb7
fix(ccusage): collect 5 user messages for AI titles, cap at 1000 chars
813687c
fix(ccusage): replace magic number with headers.length in separator row
676b0d6
fix(ccusage): filter bare URLs and condense multi-line messages for t…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| /** | ||
| * @fileoverview Utility for deriving human-readable agent IDs from usage data | ||
| * | ||
| * Produces IDs like "my-team/researcher-a1b2", "coder-c3d4", or "lead-e5f6" | ||
| * based on the teamName, agentName, and sessionId fields in JSONL entries. | ||
| * | ||
| * @module agent-id | ||
| */ | ||
|
|
||
| type AgentEntry = { | ||
| teamName?: string | undefined; | ||
| agentName?: string | undefined; | ||
| sessionId?: string | undefined; | ||
| }; | ||
|
|
||
| /** | ||
| * Derives a human-readable agent ID from a usage entry's metadata. | ||
| * | ||
| * - teamName + agentName + sessionId → `{teamName}/{agentName}-{sessionId[0:4]}` | ||
| * - only agentName + sessionId → `{agentName}-{sessionId[0:4]}` | ||
| * - neither (main agent) → `lead-{sessionId[0:4]}` | ||
| * - no sessionId → just `lead`, `{agentName}`, or `{teamName}/{agentName}` | ||
| */ | ||
| export function deriveAgentId(entry: AgentEntry): string { | ||
| const { teamName, agentName, sessionId } = entry; | ||
| const suffix = sessionId != null ? `-${sessionId.slice(0, 4)}` : ''; | ||
|
|
||
| if (teamName != null && agentName != null) { | ||
| return `${teamName}/${agentName}${suffix}`; | ||
| } | ||
| if (agentName != null) { | ||
| return `${agentName}${suffix}`; | ||
| } | ||
| return `lead${suffix}`; | ||
| } | ||
|
|
||
| /** | ||
| * Derives a role name (without session hash suffix) for grouping agent instances. | ||
| * | ||
| * - teamName + agentName → `{teamName}/{agentName}` | ||
| * - only agentName → `{agentName}` | ||
| * - neither → `lead` | ||
| */ | ||
| export function deriveAgentRole(entry: AgentEntry): string { | ||
| const { teamName, agentName } = entry; | ||
|
|
||
| if (teamName != null && agentName != null) { | ||
| return `${teamName}/${agentName}`; | ||
| } | ||
| if (agentName != null) { | ||
| return agentName; | ||
| } | ||
| return 'lead'; | ||
| } | ||
|
|
||
| // Common parent/container directories that don't identify a specific project | ||
| const CONTAINER_DIRS = new Set([ | ||
| 'Users', | ||
| 'home', | ||
| 'Projects', | ||
| 'projects', | ||
| 'IdeaProjects', | ||
| 'repos', | ||
| 'Repos', | ||
| 'repositories', | ||
| 'src', | ||
| 'code', | ||
| 'Code', | ||
| 'workspace', | ||
| 'Workspace', | ||
| 'workspaces', | ||
| 'Documents', | ||
| 'Desktop', | ||
| 'Downloads', | ||
| 'dev', | ||
| 'Dev', | ||
| 'development', | ||
| ]); | ||
|
|
||
| /** | ||
| * Extracts a short human-readable project name from an encoded project path. | ||
| * The encoded path uses '-' as a path separator (e.g. '-Users-thomas-IdeaProjects-diana'). | ||
| * Returns the last non-container segment, skipping generic directory names like 'IdeaProjects'. | ||
| */ | ||
| export function shortProjectName(encoded: string): string { | ||
| const parts = encoded.split('-').filter(Boolean); | ||
| // Find the last non-container segment | ||
| for (let i = parts.length - 1; i >= 0; i--) { | ||
| if (!CONTAINER_DIRS.has(parts[i]!)) { | ||
| // Include the preceding non-container segment for disambiguation | ||
| if (i > 0 && !CONTAINER_DIRS.has(parts[i - 1]!)) { | ||
| return `${parts[i - 1]}-${parts[i]}`; | ||
| } | ||
| return parts[i]!; | ||
| } | ||
| } | ||
| return parts.at(-1) ?? encoded; | ||
|
thomasttvo marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if (import.meta.vitest != null) { | ||
| const { describe, it, expect } = import.meta.vitest; | ||
|
|
||
| describe('deriveAgentRole', () => { | ||
| it('returns teamName/agentName when both present', () => { | ||
| expect( | ||
| deriveAgentRole({ | ||
| teamName: 'ccusage-fork', | ||
| agentName: 'researcher', | ||
| sessionId: 'a1b2c3d4', | ||
| }), | ||
| ).toBe('ccusage-fork/researcher'); | ||
| }); | ||
|
|
||
| it('returns agentName when no teamName', () => { | ||
| expect(deriveAgentRole({ agentName: 'coder', sessionId: 'deadbeef' })).toBe('coder'); | ||
| }); | ||
|
|
||
| it('returns lead for main agent', () => { | ||
| expect(deriveAgentRole({ sessionId: 'e5f6a7b8' })).toBe('lead'); | ||
| }); | ||
|
|
||
| it('returns lead when no fields present', () => { | ||
| expect(deriveAgentRole({})).toBe('lead'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('deriveAgentId', () => { | ||
| it('returns teamName/agentName-sessionPrefix when all fields present', () => { | ||
| expect( | ||
| deriveAgentId({ teamName: 'ccusage-fork', agentName: 'researcher', sessionId: 'a1b2c3d4' }), | ||
| ).toBe('ccusage-fork/researcher-a1b2'); | ||
| }); | ||
|
|
||
| it('returns agentName-sessionPrefix when no teamName', () => { | ||
| expect(deriveAgentId({ agentName: 'coder', sessionId: 'deadbeef' })).toBe('coder-dead'); | ||
| }); | ||
|
|
||
| it('returns lead-sessionPrefix for main agent (no teamName/agentName)', () => { | ||
| expect(deriveAgentId({ sessionId: 'e5f6a7b8' })).toBe('lead-e5f6'); | ||
| }); | ||
|
|
||
| it('returns lead when no fields present', () => { | ||
| expect(deriveAgentId({})).toBe('lead'); | ||
| }); | ||
|
|
||
| it('returns teamName/agentName without suffix when no sessionId', () => { | ||
| expect(deriveAgentId({ teamName: 'my-team', agentName: 'worker' })).toBe('my-team/worker'); | ||
| }); | ||
|
|
||
| it('returns agentName without suffix when no sessionId or teamName', () => { | ||
| expect(deriveAgentId({ agentName: 'solo' })).toBe('solo'); | ||
| }); | ||
|
|
||
| it('handles short sessionId gracefully', () => { | ||
| expect(deriveAgentId({ sessionId: 'ab' })).toBe('lead-ab'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('shortProjectName', () => { | ||
| it('returns last segment for normal project path', () => { | ||
| expect(shortProjectName('-Users-thomas-IdeaProjects-diana')).toBe('diana'); | ||
| }); | ||
|
|
||
| it('skips container directory IdeaProjects', () => { | ||
| expect(shortProjectName('-Users-thomas-IdeaProjects')).toBe('thomas'); | ||
| }); | ||
|
|
||
| it('skips container directory Projects', () => { | ||
| expect(shortProjectName('-Users-thomas-Projects')).toBe('thomas'); | ||
| }); | ||
|
|
||
| it('skips container directory Desktop', () => { | ||
| expect(shortProjectName('-home-user-Desktop')).toBe('user'); | ||
| }); | ||
|
|
||
| it('returns last segment when not a container', () => { | ||
| expect(shortProjectName('-Users-thomas-myapp')).toBe('thomas-myapp'); | ||
| }); | ||
|
|
||
| it('joins last two non-container segments for disambiguation', () => { | ||
| expect(shortProjectName('-Users-me-IdeaProjects-ccusage-fork')).toBe('ccusage-fork'); | ||
| }); | ||
|
|
||
| it('falls back to last segment when all are containers', () => { | ||
| expect(shortProjectName('-Projects')).toBe('Projects'); | ||
| }); | ||
|
|
||
| it('returns encoded string for empty input', () => { | ||
| expect(shortProjectName('')).toBe(''); | ||
| }); | ||
| }); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.