Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion claude_code_log/html/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
BashInput,
EditInput,
ExitPlanModeInput,
GlobInput,
MultiEditInput,
ReadInput,
TaskInput,
Expand Down Expand Up @@ -147,118 +148,151 @@ class HtmlRenderer(Renderer):
# -------------------------------------------------------------------------

def format_SystemMessage(self, message: SystemMessage) -> str:
"""Format → <div class='system-content'>...</div>."""
return format_system_content(message)

def format_HookSummaryMessage(self, message: HookSummaryMessage) -> str:
"""Format → <details class='hook-summary'>...</details>."""
return format_hook_summary_content(message)

def format_SessionHeaderMessage(self, message: SessionHeaderMessage) -> str:
"""Format → <details class='session-header'>...</details>."""
return format_session_header_content(message)

def format_DedupNoticeMessage(self, message: DedupNoticeMessage) -> str:
"""Format → <span class='muted'>...</span>."""
return format_dedup_notice_content(message)

# -------------------------------------------------------------------------
# User Content Formatters
# -------------------------------------------------------------------------

def format_UserTextMessage(self, message: UserTextMessage) -> str:
"""Format → rendered markdown HTML."""
return format_user_text_model_content(message)

def format_UserSlashCommandMessage(self, message: UserSlashCommandMessage) -> str:
"""Format → <span class='slash-command'>/cmd</span>."""
return format_user_slash_command_content(message)

def format_SlashCommandMessage(self, message: SlashCommandMessage) -> str:
"""Format → <span class='slash-command'>/cmd arg</span>."""
return format_slash_command_content(message)

def format_CommandOutputMessage(self, message: CommandOutputMessage) -> str:
"""Format → <pre class='command-output'>...</pre>."""
return format_command_output_content(message)

def format_BashInputMessage(self, message: BashInputMessage) -> str:
"""Format → <pre class='bash-input'>$ cmd</pre>."""
return format_bash_input_content(message)

def format_BashOutputMessage(self, message: BashOutputMessage) -> str:
"""Format → <pre class='bash-output'>...</pre>."""
return format_bash_output_content(message)

def format_CompactedSummaryMessage(self, message: CompactedSummaryMessage) -> str:
"""Format → <details class='compacted-summary'>...</details>."""
return format_compacted_summary_content(message)

def format_UserMemoryMessage(self, message: UserMemoryMessage) -> str:
"""Format → <details class='user-memory'>...</details>."""
return format_user_memory_content(message)

# -------------------------------------------------------------------------
# Assistant Content Formatters
# -------------------------------------------------------------------------

def format_AssistantTextMessage(self, message: AssistantTextMessage) -> str:
"""Format → rendered markdown HTML."""
return format_assistant_text_content(message)

def format_ThinkingMessage(self, message: ThinkingMessage) -> str:
"""Format → <details class='thinking'>...</details> (foldable if >10 lines)."""
return format_thinking_content(message, line_threshold=10)

def format_UnknownMessage(self, message: UnknownMessage) -> str:
"""Format → <pre class='unknown'>JSON dump</pre>."""
return format_unknown_content(message)

# -------------------------------------------------------------------------
# Tool Input Formatters
# -------------------------------------------------------------------------

def format_BashInput(self, input: BashInput) -> str:
"""Format → <pre>$ command</pre>."""
return format_bash_input(input)

def format_ReadInput(self, input: ReadInput) -> str:
"""Format → <table class='params'>file_path | ...</table>."""
return format_read_input(input)

def format_WriteInput(self, input: WriteInput) -> str:
"""Format → file path + syntax-highlighted content preview."""
return format_write_input(input)

def format_EditInput(self, input: EditInput) -> str:
"""Format → file path + diff of old_string/new_string."""
return format_edit_input(input)

def format_MultiEditInput(self, input: MultiEditInput) -> str:
"""Format → file path + multiple diffs."""
return format_multiedit_input(input)

def format_TaskInput(self, input: TaskInput) -> str:
"""Format → <div class='task-prompt'>prompt text</div>."""
return format_task_input(input)

def format_TodoWriteInput(self, input: TodoWriteInput) -> str:
"""Format → <ul class='todo-list'>...</ul>."""
return format_todowrite_input(input)

def format_AskUserQuestionInput(self, input: AskUserQuestionInput) -> str:
"""Format → questions as definition list."""
return format_askuserquestion_input(input)

def format_ExitPlanModeInput(self, input: ExitPlanModeInput) -> str:
"""Format → empty string (no content)."""
return format_exitplanmode_input(input)

def format_ToolUseContent(self, content: ToolUseContent) -> str:
"""Format → <table class='params'>key | value rows</table>."""
return render_params_table(content.input)

# -------------------------------------------------------------------------
# Tool Output Formatters
# -------------------------------------------------------------------------

def format_ReadOutput(self, output: ReadOutput) -> str:
"""Format → syntax-highlighted file content."""
return format_read_output(output)

def format_WriteOutput(self, output: WriteOutput) -> str:
"""Format → status message (e.g. 'Wrote 42 bytes')."""
return format_write_output(output)

def format_EditOutput(self, output: EditOutput) -> str:
"""Format → status message (e.g. 'Applied edit')."""
return format_edit_output(output)

def format_BashOutput(self, output: BashOutput) -> str:
"""Format → <pre>stdout/stderr</pre>."""
return format_bash_output(output)

def format_TaskOutput(self, output: TaskOutput) -> str:
"""Format → rendered markdown of task result."""
return format_task_output(output)

def format_AskUserQuestionOutput(self, output: AskUserQuestionOutput) -> str:
"""Format → user's answers as definition list."""
return format_askuserquestion_output(output)

def format_ExitPlanModeOutput(self, output: ExitPlanModeOutput) -> str:
"""Format → status message."""
return format_exitplanmode_output(output)

def format_ToolResultContent(self, output: ToolResultContent) -> str:
"""Format → <pre>raw content</pre> (fallback for unknown tools)."""
return format_tool_result_content_raw(output)

# -------------------------------------------------------------------------
Expand All @@ -278,9 +312,15 @@ def _tool_title(
return f"{prefix}{escaped_name}"

def title_TodoWriteInput(self, message: TemplateMessage) -> str: # noqa: ARG002
"""Title → '📝 Todo List'."""
return "📝 Todo List"

def title_AskUserQuestionInput(self, message: TemplateMessage) -> str: # noqa: ARG002
"""Title → '❓ Asking questions...'."""
return "❓ Asking questions..."

def title_TaskInput(self, message: TemplateMessage) -> str:
"""Title → '🔧 Task <desc> (subagent_type)'."""
content = cast(ToolUseMessage, message.content)
input = cast(TaskInput, content.input)
escaped_name = escape_html(content.tool_name)
Expand All @@ -297,18 +337,38 @@ def title_TaskInput(self, message: TemplateMessage) -> str:
return f"🔧 {escaped_name}"

def title_EditInput(self, message: TemplateMessage) -> str:
"""Title → '📝 Edit <file_path>'."""
input = cast(EditInput, cast(ToolUseMessage, message.content).input)
return self._tool_title(message, "📝", input.file_path)

def title_WriteInput(self, message: TemplateMessage) -> str:
"""Title → '📝 Write <file_path>'."""
input = cast(WriteInput, cast(ToolUseMessage, message.content).input)
return self._tool_title(message, "📝", input.file_path)

def title_ReadInput(self, message: TemplateMessage) -> str:
"""Title → '📄 Read <file_path>[, lines N-M]'."""
input = cast(ReadInput, cast(ToolUseMessage, message.content).input)
return self._tool_title(message, "📄", input.file_path)
summary = input.file_path
# Add line range info if available
if input.limit is not None:
offset = input.offset or 0
if input.limit == 1:
summary = f"{summary}, line {offset + 1}"
else:
summary = f"{summary}, lines {offset + 1}-{offset + input.limit}"
return self._tool_title(message, "📄", summary)

def title_GlobInput(self, message: TemplateMessage) -> str:
"""Title → '🔍 Glob <pattern>[ in path]'."""
input = cast(GlobInput, cast(ToolUseMessage, message.content).input)
summary = input.pattern
if input.path:
summary = f"{summary} in {input.path}"
return self._tool_title(message, "🔍", summary)

def title_BashInput(self, message: TemplateMessage) -> str:
"""Title → '💻 Bash <description>'."""
input = cast(BashInput, cast(ToolUseMessage, message.content).input)
return self._tool_title(message, "💻", input.description)

Expand Down
3 changes: 2 additions & 1 deletion claude_code_log/html/templates/components/global_styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
--question-accent: #f5a623;
--question-bg: #fffbf0;
--answer-accent: #4caf50;
--answer-bg: #f0fff4;
--answer-bg: #fffbf0;

/* Priority palette (purple intensity - darker = more urgent) */
--priority-600: #7c3aed;
Expand All @@ -66,6 +66,7 @@
--text-primary: #333;
--text-muted: #666;
--text-secondary: #495057;
--fold-color: #888;

/* Border colors */
--border-light: #e0e0e0;
Expand Down
16 changes: 6 additions & 10 deletions claude_code_log/html/templates/components/message_styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,22 @@
flex: 1;
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-end;
gap: 0.4em;
cursor: pointer;
user-select: none;
font-size: 0.9em;
font-weight: 500;
padding: 0.4em;
padding: 0.4em 0.8em 0.4em 0.4em;
transition: all 0.2s ease;
border-bottom: 2px solid transparent;
border-bottom: 1px solid transparent;
background: linear-gradient(to bottom, #f8f8f844, #f0f0f0);
}

/* Show border only when folded (content is hidden) */
.fold-bar-section.folded {
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-width: 1px;
}

.fold-bar-section:hover {
Expand All @@ -72,6 +72,7 @@
.fold-icon {
font-size: 1.1em;
line-height: 1;
color: var(--fold-color);
}

.fold-count {
Expand All @@ -81,7 +82,7 @@
}

.fold-label {
color: var(--text-muted);
color: var(--fold-color);
font-size: 0.9em;
}

Expand Down Expand Up @@ -538,11 +539,6 @@
}

.thinking {
border-left-color: var(--assistant-dimmed);
}

/* Full purple when thinking is paired (as pair_first) */
.thinking.pair_first {
border-left-color: var(--assistant-color);
}

Expand Down
16 changes: 10 additions & 6 deletions claude_code_log/html/templates/components/todo_styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
}

.todo-item.medium {
border-left: 3px solid var(--priority-medium);
border-left: 3px solid transparent;
}

.todo-item.low {
Expand All @@ -102,17 +102,17 @@
padding: 12px;
background-color: var(--question-bg);
border-radius: 6px;
border-left: 3px solid var(--question-accent);
border-left: 3px solid var(--assistant-color);
}

.question-block:last-child {
margin-bottom: 0;
}

/* Answered questions in result (lighter, success-tinted) */
/* Answered questions in result */
.question-block.answered {
background-color: var(--answer-bg);
border-left-color: var(--answer-accent);
border-left-color: var(--user-color);
}

.question-header {
Expand All @@ -135,9 +135,13 @@
.answer-text {
font-size: 1.05em;
font-weight: 600;
color: var(--answer-accent);
color: var(--text-primary);
line-height: 1.4;
padding-left: 4px;
}

/* Q: and A: labels */
.qa-label {
font-weight: 700;
}

.question-options-hint {
Expand Down
22 changes: 16 additions & 6 deletions claude_code_log/html/tool_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ def _render_question_item(q: AskUserQuestionItem) -> str:
escaped_header = escape_html(q.header)
html_parts.append(f'<div class="question-header">{escaped_header}</div>')

# Question text with icon
# Question text with Q: label
question_text = escape_html(q.question)
html_parts.append(f'<div class="question-text">❓ {question_text}</div>')
html_parts.append(
f'<div class="question-text"><span class="qa-label">Q:</span> {question_text}</div>'
)

# Options (if present)
if q.options:
Expand Down Expand Up @@ -155,8 +157,12 @@ def format_askuserquestion_result(content: str) -> str:
escaped_q = escape_html(question)
escaped_a = escape_html(answer)
html_parts.append('<div class="question-block answered">')
html_parts.append(f'<div class="question-text">❓ {escaped_q}</div>')
html_parts.append(f'<div class="answer-text">✅ {escaped_a}</div>')
html_parts.append(
f'<div class="question-text"><span class="qa-label">Q:</span> {escaped_q}</div>'
)
html_parts.append(
f'<div class="answer-text"><span class="qa-label answer">A:</span> {escaped_a}</div>'
)
html_parts.append("</div>")

html_parts.append("</div>")
Expand Down Expand Up @@ -391,8 +397,12 @@ def format_askuserquestion_output(output: AskUserQuestionOutput) -> str:
escaped_q = escape_html(qa.question)
escaped_a = escape_html(qa.answer)
html_parts.append('<div class="question-block answered">')
html_parts.append(f'<div class="question-text">❓ {escaped_q}</div>')
html_parts.append(f'<div class="answer-text">✅ {escaped_a}</div>')
html_parts.append(
f'<div class="question-text"><span class="qa-label">Q:</span> {escaped_q}</div>'
)
html_parts.append(
f'<div class="answer-text"><span class="qa-label answer">A:</span> {escaped_a}</div>'
)
html_parts.append("</div>")

html_parts.append("</div>")
Expand Down
Loading
Loading