Skip to content

Commit f107f30

Browse files
committed
feat: consolidate account and session workbenches
- add unified account import and deep-link apply adapter - add session analysis/plugin console and paged session detail loading - refine Codex account detail desktop draft and design-system inspect mode
1 parent e318ac2 commit f107f30

79 files changed

Lines changed: 9529 additions & 611 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/gettokens-domain-engineering/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ This skill unifies the technical rules for building, styling, and debugging GetT
147147
- Treat `~/.codex/sessions`, `~/.codex/archived_sessions`, and `~/.claude/projects` as potentially multi-GB local stores. Never make page entry depend on synchronous full-file parsing when a bounded snapshot or stale cache can satisfy the first paint.
148148
- Snapshot/list APIs should return summaries only. Do not carry full message bodies, raw tool payloads, or per-message `content` through snapshot DTOs.
149149
- Detail APIs used by the UI must be payload-bounded. Keep full parsing inside backend-only analysis paths when needed, but UI detail responses should prefer summary rows, cap message count, and avoid returning full `content` unless a scoped requirement explicitly needs it.
150+
- If the UI needs full session-detail visibility, split metadata from messages. `Get*SessionDetail` should return only metadata/counts, while a separate message-page API reads JSONL line windows (`offset / limit`) and stops after the current page plus one lookahead row for `hasMore`. Frontend detail views should append pages on demand and render only the loaded window.
151+
- Raw JSONL inspection must be per-message and on demand. Message-page DTOs may carry a source `lineNumber`, but raw JSON should be fetched through a separate line-read API only after explicit user interaction, never bundled into list/detail/page payloads.
150152
- Any in-process cache for session details must have both entry-count and approximate-byte limits. Oversized details should use disk cache only, not stay resident in `App`.
151153
- Disk caches must invalidate by file fingerprint such as size plus mtime. Do not use session id alone as a cache key for mutable JSONL files.
152154
- Regression coverage for this class should include cache hit, stale-cache invalidation, payload compaction, memory-bound eviction, and at least one live benchmark path gated by an explicit environment variable so CI never reads a developer's real sessions.
@@ -327,6 +329,8 @@ This skill unifies the technical rules for building, styling, and debugging GetT
327329
- Do not put cards inside account details just to create section boundaries. Use section density (`standard`, `dense`, `hero`), table/grid rows, and overview evidence modules instead.
328330
- Save actions for detail-page modules should follow the page/modal footer when the edit affects persistent account configuration. Individual sections may keep local actions such as add row, delete draft row, verify, fetch models, or copy.
329331
- OpenAI-compatible and Codex route-row details are account detail variants; keep them visually aligned with `UnifiedAccountDetailModal` even when their controller/state logic remains separate.
332+
- For API-key-like details, keep credential editing and connection verification in one `AccountCredentialVerifySection` when they operate on the same draft. Avoid adjacent `Credentials` / `Verify` cards that force users to scan two modules for one setup task.
333+
- For explicitly desktop-only account detail drafts, validate desktop density and overflow only. Do not introduce phone-width layout work or 375px screenshot acceptance unless the product requirement reintroduces mobile support.
330334
- Account creation/configuration modals such as `UnifiedComposeModal` should reuse account detail primitives instead of hand-rolled form shells. Keep configuration flows in named sections, localize visible menu labels and section eyebrows, and preserve the existing submit callbacks while changing layout.
331335
- OpenAI-compatible provider details should defer runtime/evidence split until very wide viewports and keep model rows responsive: model and alias inputs may split at medium width, but destructive row actions must stay horizontal and only join the row when there is enough width.
332336
- **Rotation Cards**: `AccountRotationModal` is a variant of the account card, not a second visual system. Reuse the account-card content hierarchy and only replace the bottom action strip plus rotation-only affordances such as rank rail and drag marker.

app.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,40 @@ func (a *App) GetCodexSessionDetail(sessionID string) (*SessionManagementSession
618618
return mapSessionManagementSessionDetail(result), nil
619619
}
620620

621+
func (a *App) GetCodexSessionMessagePage(sessionID string, input SessionManagementMessagePageInput) (*SessionManagementMessagePage, error) {
622+
result, err := a.core.GetCodexSessionMessagePage(sessionID, wailsapp.SessionManagementMessagePageInput{
623+
Offset: input.Offset,
624+
Limit: input.Limit,
625+
})
626+
if err != nil {
627+
return nil, err
628+
}
629+
return mapSessionManagementMessagePage(result), nil
630+
}
631+
632+
func (a *App) GetCodexSessionMessageRawJSON(sessionID string, input SessionManagementMessageRawJSONInput) (*SessionManagementMessageRawJSON, error) {
633+
result, err := a.core.GetCodexSessionMessageRawJSON(sessionID, wailsapp.SessionManagementMessageRawJSONInput{
634+
LineNumber: input.LineNumber,
635+
})
636+
if err != nil {
637+
return nil, err
638+
}
639+
return mapSessionManagementMessageRawJSON(result), nil
640+
}
641+
642+
func (a *App) AnalyzeCodexSessions(input AnalyzeCodexSessionsInput) (*SessionAnalysisResult, error) {
643+
result, err := a.core.AnalyzeCodexSessions(wailsapp.AnalyzeCodexSessionsInput{
644+
Scope: input.Scope,
645+
ProjectID: input.ProjectID,
646+
SessionIDs: append([]string(nil), input.SessionIDs...),
647+
Limit: input.Limit,
648+
})
649+
if err != nil {
650+
return nil, err
651+
}
652+
return mapSessionAnalysisResult(result), nil
653+
}
654+
621655
func (a *App) GetClaudeCodeSessionDetail(sessionID string) (*SessionManagementSessionDetail, error) {
622656
result, err := a.core.GetClaudeCodeSessionDetail(sessionID)
623657
if err != nil {
@@ -626,6 +660,27 @@ func (a *App) GetClaudeCodeSessionDetail(sessionID string) (*SessionManagementSe
626660
return mapSessionManagementSessionDetail(result), nil
627661
}
628662

663+
func (a *App) GetClaudeCodeSessionMessagePage(sessionID string, input SessionManagementMessagePageInput) (*SessionManagementMessagePage, error) {
664+
result, err := a.core.GetClaudeCodeSessionMessagePage(sessionID, wailsapp.SessionManagementMessagePageInput{
665+
Offset: input.Offset,
666+
Limit: input.Limit,
667+
})
668+
if err != nil {
669+
return nil, err
670+
}
671+
return mapSessionManagementMessagePage(result), nil
672+
}
673+
674+
func (a *App) GetClaudeCodeSessionMessageRawJSON(sessionID string, input SessionManagementMessageRawJSONInput) (*SessionManagementMessageRawJSON, error) {
675+
result, err := a.core.GetClaudeCodeSessionMessageRawJSON(sessionID, wailsapp.SessionManagementMessageRawJSONInput{
676+
LineNumber: input.LineNumber,
677+
})
678+
if err != nil {
679+
return nil, err
680+
}
681+
return mapSessionManagementMessageRawJSON(result), nil
682+
}
683+
629684
func (a *App) UpdateCodexSessionProviders(input UpdateSessionProvidersInput) (*SessionManagementSnapshot, error) {
630685
result, err := a.core.UpdateCodexSessionProviders(wailsapp.UpdateSessionProvidersInput{
631686
ProjectID: input.ProjectID,

app_mappers.go

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -902,14 +902,15 @@ func mapSessionManagementSessionDetail(result *wailsapp.SessionManagementSession
902902
messages := make([]SessionManagementMessageRecord, 0, len(result.Messages))
903903
for _, message := range result.Messages {
904904
messages = append(messages, SessionManagementMessageRecord{
905-
ID: message.ID,
906-
Role: message.Role,
907-
TimeLabel: message.TimeLabel,
908-
Timestamp: message.Timestamp,
909-
Title: message.Title,
910-
Summary: message.Summary,
911-
Content: message.Content,
912-
Truncated: message.Truncated,
905+
ID: message.ID,
906+
LineNumber: message.LineNumber,
907+
Role: message.Role,
908+
TimeLabel: message.TimeLabel,
909+
Timestamp: message.Timestamp,
910+
Title: message.Title,
911+
Summary: message.Summary,
912+
Content: message.Content,
913+
Truncated: message.Truncated,
913914
})
914915
}
915916

@@ -935,6 +936,135 @@ func mapSessionManagementSessionDetail(result *wailsapp.SessionManagementSession
935936
}
936937
}
937938

939+
func mapSessionManagementMessagePage(result *wailsapp.SessionManagementMessagePage) *SessionManagementMessagePage {
940+
if result == nil {
941+
return &SessionManagementMessagePage{
942+
Messages: []SessionManagementMessageRecord{},
943+
}
944+
}
945+
messages := make([]SessionManagementMessageRecord, 0, len(result.Messages))
946+
for _, message := range result.Messages {
947+
messages = append(messages, SessionManagementMessageRecord{
948+
ID: message.ID,
949+
LineNumber: message.LineNumber,
950+
Role: message.Role,
951+
TimeLabel: message.TimeLabel,
952+
Timestamp: message.Timestamp,
953+
Title: message.Title,
954+
Summary: message.Summary,
955+
Content: message.Content,
956+
Truncated: message.Truncated,
957+
})
958+
}
959+
return &SessionManagementMessagePage{
960+
SessionID: result.SessionID,
961+
Offset: result.Offset,
962+
Limit: result.Limit,
963+
MessageCount: result.MessageCount,
964+
NextOffset: result.NextOffset,
965+
HasMore: result.HasMore,
966+
Messages: messages,
967+
}
968+
}
969+
970+
func mapSessionManagementMessageRawJSON(result *wailsapp.SessionManagementMessageRawJSON) *SessionManagementMessageRawJSON {
971+
if result == nil {
972+
return &SessionManagementMessageRawJSON{}
973+
}
974+
return &SessionManagementMessageRawJSON{
975+
SessionID: result.SessionID,
976+
LineNumber: result.LineNumber,
977+
RawJSON: result.RawJSON,
978+
}
979+
}
980+
981+
func mapSessionAnalysisResult(result *wailsapp.SessionAnalysisResult) *SessionAnalysisResult {
982+
if result == nil {
983+
return &SessionAnalysisResult{
984+
Keywords: []SessionAnalysisKeyword{},
985+
RoleContributions: []SessionAnalysisRoleContribution{},
986+
Projects: []SessionAnalysisProjectSummary{},
987+
Sessions: []SessionAnalysisSessionSummary{},
988+
}
989+
}
990+
991+
projects := make([]SessionAnalysisProjectSummary, 0, len(result.Projects))
992+
for _, project := range result.Projects {
993+
projects = append(projects, SessionAnalysisProjectSummary{
994+
ProjectID: project.ProjectID,
995+
ProjectName: project.ProjectName,
996+
SessionCount: project.SessionCount,
997+
MessageCount: project.MessageCount,
998+
TermCount: project.TermCount,
999+
Keywords: mapSessionAnalysisKeywords(project.Keywords),
1000+
})
1001+
}
1002+
1003+
sessions := make([]SessionAnalysisSessionSummary, 0, len(result.Sessions))
1004+
for _, session := range result.Sessions {
1005+
sessions = append(sessions, SessionAnalysisSessionSummary{
1006+
SessionID: session.SessionID,
1007+
ProjectID: session.ProjectID,
1008+
ProjectName: session.ProjectName,
1009+
Title: session.Title,
1010+
Status: session.Status,
1011+
Provider: session.Provider,
1012+
Model: session.Model,
1013+
MessageCount: session.MessageCount,
1014+
TermCount: session.TermCount,
1015+
TopicLine: session.TopicLine,
1016+
Keywords: mapSessionAnalysisKeywords(session.Keywords),
1017+
RoleContributions: mapSessionAnalysisRoleContributions(session.RoleContributions),
1018+
})
1019+
}
1020+
1021+
return &SessionAnalysisResult{
1022+
Scope: result.Scope,
1023+
GeneratedAt: result.GeneratedAt,
1024+
RequestedSessionCount: result.RequestedSessionCount,
1025+
AnalyzedSessionCount: result.AnalyzedSessionCount,
1026+
SkippedSessionCount: result.SkippedSessionCount,
1027+
TotalMessages: result.TotalMessages,
1028+
TotalTerms: result.TotalTerms,
1029+
Keywords: mapSessionAnalysisKeywords(result.Keywords),
1030+
RoleContributions: mapSessionAnalysisRoleContributions(result.RoleContributions),
1031+
Projects: projects,
1032+
Sessions: sessions,
1033+
}
1034+
}
1035+
1036+
func mapSessionAnalysisKeywords(items []wailsapp.SessionAnalysisKeyword) []SessionAnalysisKeyword {
1037+
if len(items) == 0 {
1038+
return []SessionAnalysisKeyword{}
1039+
}
1040+
out := make([]SessionAnalysisKeyword, 0, len(items))
1041+
for _, item := range items {
1042+
out = append(out, SessionAnalysisKeyword{
1043+
Term: item.Term,
1044+
Count: item.Count,
1045+
SessionCount: item.SessionCount,
1046+
Score: item.Score,
1047+
})
1048+
}
1049+
return out
1050+
}
1051+
1052+
func mapSessionAnalysisRoleContributions(items []wailsapp.SessionAnalysisRoleContribution) []SessionAnalysisRoleContribution {
1053+
if len(items) == 0 {
1054+
return []SessionAnalysisRoleContribution{}
1055+
}
1056+
out := make([]SessionAnalysisRoleContribution, 0, len(items))
1057+
for _, item := range items {
1058+
out = append(out, SessionAnalysisRoleContribution{
1059+
Role: item.Role,
1060+
MessageCount: item.MessageCount,
1061+
TermCount: item.TermCount,
1062+
Share: item.Share,
1063+
})
1064+
}
1065+
return out
1066+
}
1067+
9381068
func cloneProviderCountMap(source map[string]int) map[string]int {
9391069
if len(source) == 0 {
9401070
return map[string]int{}

app_types.go

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,65 @@ type UpdateSessionProvidersInput struct {
10171017
Snapshot *SessionManagementSnapshot `json:"snapshot,omitempty"`
10181018
}
10191019

1020+
type AnalyzeCodexSessionsInput struct {
1021+
Scope string `json:"scope"`
1022+
ProjectID string `json:"projectID,omitempty"`
1023+
SessionIDs []string `json:"sessionIDs,omitempty"`
1024+
Limit int `json:"limit,omitempty"`
1025+
}
1026+
1027+
type SessionAnalysisResult struct {
1028+
Scope string `json:"scope"`
1029+
GeneratedAt string `json:"generatedAt"`
1030+
RequestedSessionCount int `json:"requestedSessionCount"`
1031+
AnalyzedSessionCount int `json:"analyzedSessionCount"`
1032+
SkippedSessionCount int `json:"skippedSessionCount"`
1033+
TotalMessages int `json:"totalMessages"`
1034+
TotalTerms int `json:"totalTerms"`
1035+
Keywords []SessionAnalysisKeyword `json:"keywords"`
1036+
RoleContributions []SessionAnalysisRoleContribution `json:"roleContributions"`
1037+
Projects []SessionAnalysisProjectSummary `json:"projects"`
1038+
Sessions []SessionAnalysisSessionSummary `json:"sessions"`
1039+
}
1040+
1041+
type SessionAnalysisKeyword struct {
1042+
Term string `json:"term"`
1043+
Count int `json:"count"`
1044+
SessionCount int `json:"sessionCount"`
1045+
Score float64 `json:"score"`
1046+
}
1047+
1048+
type SessionAnalysisRoleContribution struct {
1049+
Role string `json:"role"`
1050+
MessageCount int `json:"messageCount"`
1051+
TermCount int `json:"termCount"`
1052+
Share float64 `json:"share"`
1053+
}
1054+
1055+
type SessionAnalysisProjectSummary struct {
1056+
ProjectID string `json:"projectID"`
1057+
ProjectName string `json:"projectName"`
1058+
SessionCount int `json:"sessionCount"`
1059+
MessageCount int `json:"messageCount"`
1060+
TermCount int `json:"termCount"`
1061+
Keywords []SessionAnalysisKeyword `json:"keywords"`
1062+
}
1063+
1064+
type SessionAnalysisSessionSummary struct {
1065+
SessionID string `json:"sessionID"`
1066+
ProjectID string `json:"projectID"`
1067+
ProjectName string `json:"projectName"`
1068+
Title string `json:"title"`
1069+
Status string `json:"status"`
1070+
Provider string `json:"provider"`
1071+
Model string `json:"model,omitempty"`
1072+
MessageCount int `json:"messageCount"`
1073+
TermCount int `json:"termCount"`
1074+
TopicLine string `json:"topicLine"`
1075+
Keywords []SessionAnalysisKeyword `json:"keywords"`
1076+
RoleContributions []SessionAnalysisRoleContribution `json:"roleContributions"`
1077+
}
1078+
10201079
type SessionManagementSnapshot struct {
10211080
ProjectCount int `json:"projectCount"`
10221081
SessionCount int `json:"sessionCount"`
@@ -1087,14 +1146,40 @@ type SessionManagementSessionDetail struct {
10871146
}
10881147

10891148
type SessionManagementMessageRecord struct {
1090-
ID string `json:"id"`
1091-
Role string `json:"role"`
1092-
TimeLabel string `json:"timeLabel"`
1093-
Timestamp string `json:"timestamp,omitempty"`
1094-
Title string `json:"title"`
1095-
Summary string `json:"summary"`
1096-
Content string `json:"content"`
1097-
Truncated bool `json:"truncated,omitempty"`
1149+
ID string `json:"id"`
1150+
LineNumber int `json:"lineNumber,omitempty"`
1151+
Role string `json:"role"`
1152+
TimeLabel string `json:"timeLabel"`
1153+
Timestamp string `json:"timestamp,omitempty"`
1154+
Title string `json:"title"`
1155+
Summary string `json:"summary"`
1156+
Content string `json:"content"`
1157+
Truncated bool `json:"truncated,omitempty"`
1158+
}
1159+
1160+
type SessionManagementMessageRawJSONInput struct {
1161+
LineNumber int `json:"lineNumber"`
1162+
}
1163+
1164+
type SessionManagementMessageRawJSON struct {
1165+
SessionID string `json:"sessionID"`
1166+
LineNumber int `json:"lineNumber"`
1167+
RawJSON string `json:"rawJSON"`
1168+
}
1169+
1170+
type SessionManagementMessagePageInput struct {
1171+
Offset int `json:"offset"`
1172+
Limit int `json:"limit"`
1173+
}
1174+
1175+
type SessionManagementMessagePage struct {
1176+
SessionID string `json:"sessionID"`
1177+
Offset int `json:"offset"`
1178+
Limit int `json:"limit"`
1179+
MessageCount int `json:"messageCount"`
1180+
NextOffset int `json:"nextOffset"`
1181+
HasMore bool `json:"hasMore"`
1182+
Messages []SessionManagementMessageRecord `json:"messages"`
10981183
}
10991184

11001185
// CLAUDE.md Memory File types

docs-linhay/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,7 @@ qmd update && qmd embed
8181
6. 文档改动完成后:运行 `check-docs.sh`
8282
7. memory 写回完成后:运行 `qmd update && qmd embed`
8383
8. 需求进入并行实现后:为对应 `space-key` 创建同 key 的 branch 和 `worktree`
84+
85+
## 当前 Space
86+
87+
- [20260528-account-import-unified-modal](spaces/20260528-account-import-unified-modal/README.md)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Design System Inspect Mode
2+
3+
## 背景
4+
5+
设计系统已有两条调试链路:
6+
7+
1. Storybook 作为组件工作台。
8+
2. 应用开发态通过 `@linhey/react-debug-inspector` 注入 `data-debug`,并显示右下角组件定位器。
9+
10+
本次补齐的是“从设计系统入口直接进入元素定位”的产品化入口,避免开发者知道底层工具但找不到启动路径。
11+
12+
## 实现边界
13+
14+
1. `frontend/src/features/design-system/storyCatalog.ts` 新增 inspect URL 解析:`/?inspect=design-system#frame=design-system`
15+
2. `frontend/src/features/design-system/DesignSystemEntryFeature.tsx` 在开发态显示“定位元素”入口。
16+
3. `frontend/src/features/design-system/inspectMode.ts` 包装 `initInspector()`,在开发态读取 `inspect=design-system` 后触发第三方 inspector 的定位按钮。
17+
4. `frontend/src/main.tsx` 只在 `import.meta.env.DEV` 下初始化 inspect bridge,生产构建不启用。
18+
19+
## 验收
20+
21+
1. 单元测试锁定 storyCatalog URL、main 初始化、Vite `createViteDebugInspectorPlugin()` 和入口按钮。
22+
2. 浏览器 DOM 验收:`http://127.0.0.1:5174/?inspect=design-system#frame=design-system` 自动设置 `data-design-system-inspect-mode="active"`,桥接对象存在,页面含 `data-debug` 节点。
23+
3. 截图归档:`docs-linhay/spaces/20260528-design-system-inspect-mode/screenshots/20260528/design-system/20260528-design-system-inspect-mode-after-v01.png`
24+
25+
## 后续边界
26+
27+
Storybook 内部 toolbar / addon 不在本期范围。若后续需要在 Storybook iframe 内直接启动定位,应单独设计 Storybook preview 层的注入与快捷键协议,不复用应用主 frame 的 URL query。

0 commit comments

Comments
 (0)