mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
581df433ed | ||
|
|
d0c8e060ff | ||
|
|
cdd6b21d3b | ||
|
|
b5f35214ef | ||
|
|
2f73a94808 | ||
|
|
611c06f31b | ||
|
|
7f41bcbd9c | ||
|
|
f47b9f3824 |
No files matched your search
@@ -0,0 +1,290 @@
|
||||
name: "Claude Code Action"
|
||||
description: "Run Claude Code in GitHub Actions workflows"
|
||||
|
||||
inputs:
|
||||
anthropic_api_key:
|
||||
description: "Anthropic API key"
|
||||
required: false
|
||||
claude_code_oauth_token:
|
||||
description: "Claude Code OAuth Token for Claude Pro"
|
||||
required: false
|
||||
github_token:
|
||||
description: "GitHub token for Claude to operate with"
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
trigger_phrase:
|
||||
description: "The trigger phrase to look for in comments, issue/PR bodies, and issue titles"
|
||||
required: false
|
||||
default: "@claude"
|
||||
assignee_trigger:
|
||||
description: "The assignee username that triggers the action (e.g. @claude). Only used for issue assignment"
|
||||
required: false
|
||||
max_turns:
|
||||
description: "Maximum number of conversation turns Claude can take"
|
||||
required: false
|
||||
timeout_minutes:
|
||||
description: "Timeout in minutes for execution"
|
||||
required: false
|
||||
default: "30"
|
||||
model:
|
||||
description: "Model to use (provider-specific format required for Bedrock/Vertex)"
|
||||
required: false
|
||||
use_bedrock:
|
||||
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
default: "false"
|
||||
use_vertex:
|
||||
description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
default: "false"
|
||||
allowed_tools:
|
||||
description: "Additional tools for Claude to use (the base GitHub tools will always be included)"
|
||||
required: false
|
||||
default: ""
|
||||
disallowed_tools:
|
||||
description: "Tools that Claude should never use"
|
||||
required: false
|
||||
default: ""
|
||||
custom_instructions:
|
||||
description: "Additional custom instructions to include in the prompt for Claude"
|
||||
required: false
|
||||
default: ""
|
||||
mcp_config:
|
||||
description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers"
|
||||
required: false
|
||||
default: ""
|
||||
claude_env:
|
||||
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
|
||||
required: false
|
||||
default: ""
|
||||
direct_prompt:
|
||||
description: "Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows)"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Claude Code
|
||||
shell: bash
|
||||
run: npm install -g @anthropic-ai/claude-code
|
||||
|
||||
- name: Install GitHub MCP Server
|
||||
shell: bash
|
||||
run: |
|
||||
claude mcp add-json github '{
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-ff3036d"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ inputs.github_token }}"
|
||||
}
|
||||
}'
|
||||
|
||||
- name: Extract GitHub Context and Create Prompt
|
||||
shell: bash
|
||||
id: prepare_context
|
||||
env:
|
||||
# Pass user-controlled inputs via environment variables to prevent script injection (GHSL-2025-093)
|
||||
EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
EVENT_ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
EVENT_ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
EVENT_PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
EVENT_REVIEW_BODY: ${{ github.event.review.body }}
|
||||
EVENT_ASSIGNEE_LOGIN: ${{ github.event.assignee.login }}
|
||||
INPUT_TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
||||
INPUT_ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
||||
INPUT_DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
||||
EVENT_ACTION: ${{ github.event.action }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
run: |
|
||||
echo "🔍 Extracting GitHub context from event: $EVENT_NAME"
|
||||
|
||||
# Function to check for trigger phrase
|
||||
check_trigger() {
|
||||
local text="$1"
|
||||
local trigger="$INPUT_TRIGGER_PHRASE"
|
||||
if [[ "$text" == *"$trigger"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Extract context based on event type
|
||||
TRIGGER_FOUND="false"
|
||||
USER_REQUEST=""
|
||||
CONTEXT_INFO=""
|
||||
|
||||
case "$EVENT_NAME" in
|
||||
"issue_comment")
|
||||
COMMENT_BODY="$EVENT_COMMENT_BODY"
|
||||
ISSUE_TITLE="$EVENT_ISSUE_TITLE"
|
||||
ISSUE_NUMBER="$EVENT_ISSUE_NUMBER"
|
||||
|
||||
if check_trigger "$COMMENT_BODY"; then
|
||||
TRIGGER_FOUND="true"
|
||||
USER_REQUEST="$COMMENT_BODY"
|
||||
CONTEXT_INFO="Issue Comment on #$ISSUE_NUMBER: $ISSUE_TITLE"
|
||||
fi
|
||||
;;
|
||||
|
||||
"pull_request_review_comment")
|
||||
COMMENT_BODY="$EVENT_COMMENT_BODY"
|
||||
PR_TITLE="$EVENT_PR_TITLE"
|
||||
PR_NUMBER="$EVENT_PR_NUMBER"
|
||||
|
||||
if check_trigger "$COMMENT_BODY"; then
|
||||
TRIGGER_FOUND="true"
|
||||
USER_REQUEST="$COMMENT_BODY"
|
||||
CONTEXT_INFO="PR Comment on #$PR_NUMBER: $PR_TITLE"
|
||||
fi
|
||||
;;
|
||||
|
||||
"pull_request_review")
|
||||
REVIEW_BODY="$EVENT_REVIEW_BODY"
|
||||
PR_TITLE="$EVENT_PR_TITLE"
|
||||
PR_NUMBER="$EVENT_PR_NUMBER"
|
||||
|
||||
if check_trigger "$REVIEW_BODY"; then
|
||||
TRIGGER_FOUND="true"
|
||||
USER_REQUEST="$REVIEW_BODY"
|
||||
CONTEXT_INFO="PR Review on #$PR_NUMBER: $PR_TITLE"
|
||||
fi
|
||||
;;
|
||||
|
||||
"issues")
|
||||
ISSUE_BODY="$EVENT_ISSUE_BODY"
|
||||
ISSUE_TITLE="$EVENT_ISSUE_TITLE"
|
||||
ISSUE_NUMBER="$EVENT_ISSUE_NUMBER"
|
||||
|
||||
if check_trigger "$ISSUE_TITLE" || check_trigger "$ISSUE_BODY"; then
|
||||
TRIGGER_FOUND="true"
|
||||
USER_REQUEST="$ISSUE_BODY"
|
||||
CONTEXT_INFO="Issue #$ISSUE_NUMBER: $ISSUE_TITLE"
|
||||
elif [[ "$EVENT_ACTION" == "assigned" && -n "$INPUT_ASSIGNEE_TRIGGER" ]]; then
|
||||
ASSIGNEE="$EVENT_ASSIGNEE_LOGIN"
|
||||
if [[ "$ASSIGNEE" == "$INPUT_ASSIGNEE_TRIGGER" ]]; then
|
||||
TRIGGER_FOUND="true"
|
||||
USER_REQUEST="$ISSUE_BODY"
|
||||
CONTEXT_INFO="Issue #$ISSUE_NUMBER assigned to $ASSIGNEE: $ISSUE_TITLE"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check for direct prompt override
|
||||
if [[ -n "$INPUT_DIRECT_PROMPT" ]]; then
|
||||
TRIGGER_FOUND="true"
|
||||
USER_REQUEST="$INPUT_DIRECT_PROMPT"
|
||||
CONTEXT_INFO="Automated GitHub workflow"
|
||||
fi
|
||||
|
||||
if [[ "$TRIGGER_FOUND" != "true" ]]; then
|
||||
echo "❌ No trigger phrase found or direct prompt provided. Exiting gracefully."
|
||||
echo "SKIP_EXECUTION=true" >> $GITHUB_ENV
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "✅ Trigger found! Context: $CONTEXT_INFO"
|
||||
|
||||
# Create comprehensive prompt
|
||||
mkdir -p /tmp/claude-action
|
||||
cat > /tmp/claude-action/github-context-prompt.txt << EOF
|
||||
You are Claude Code, an AI assistant helping with GitHub workflows and code.
|
||||
|
||||
Repository: $REPO_NAME
|
||||
Context: $CONTEXT_INFO
|
||||
Event: $EVENT_NAME
|
||||
|
||||
User Request:
|
||||
$USER_REQUEST
|
||||
|
||||
Please analyze the request and provide helpful assistance.
|
||||
|
||||
CRITICAL INSTRUCTION: If your task involves modifying codebase files to add a feature or fix a bug:
|
||||
|
||||
STEP 0 — SETUP GIT IDENTITY:
|
||||
git config user.name "MervinPraison"
|
||||
git config user.email "[email protected]"
|
||||
|
||||
STEP 1 — READ GUIDELINES:
|
||||
MANDATORY: Before writing any code, you MUST carefully read "src/praisonai-agents/AGENTS.md" to understand this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy.
|
||||
|
||||
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
|
||||
Before implementing anything, answer these questions:
|
||||
- CORE vs WRAPPER vs TOOLS ROUTING:
|
||||
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
|
||||
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
|
||||
3. Tools (PraisonAI-Tools): Specific integrations, external tools (e.g. SurrealDB, Slack), and community extensions MUST go to the `MervinPraison/PraisonAI-Tools` repository. If the feature is a tool, clone `https://github.com/MervinPraison/PraisonAI-Tools`, implement it there, and create the PR using `gh pr create -R MervinPraison/PraisonAI-Tools`.
|
||||
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory).
|
||||
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
|
||||
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
|
||||
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead.
|
||||
- Is there a name collision with existing exports in __init__.py?
|
||||
If ANY of these conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
|
||||
|
||||
STEP 3 — IMPLEMENT:
|
||||
- Create a new git branch appropriately named (e.g. \`feature/issue-<number>\`)
|
||||
- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper
|
||||
- Keep changes small and backward-compatible
|
||||
|
||||
STEP 4 — TEST:
|
||||
- Run: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
|
||||
- Ensure no regressions
|
||||
|
||||
STEP 5 — CREATE PR:
|
||||
- Commit with descriptive message, push the branch
|
||||
- Use the GitHub CLI (\`gh pr create --title "..." --body "Fixes #<number>"\`) to open a Pull Request
|
||||
CRITICAL: You MUST create the PR automatically using \`gh pr create\`. Do NOT just provide a link.
|
||||
|
||||
Respond naturally and helpfully to the user's request using the available tools.
|
||||
EOF
|
||||
|
||||
echo "PROMPT_FILE=/tmp/claude-action/github-context-prompt.txt" >> $GITHUB_ENV
|
||||
echo "SKIP_EXECUTION=false" >> $GITHUB_ENV
|
||||
|
||||
- name: Run Claude Code
|
||||
if: env.SKIP_EXECUTION != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "🚀 Running Claude Code with GitHub context..."
|
||||
|
||||
# Build command arguments
|
||||
CMD_ARGS=("-p" "--verbose" "--output-format" "stream-json")
|
||||
|
||||
# Add max turns if specified
|
||||
if [[ -n "${{ inputs.max_turns }}" ]]; then
|
||||
CMD_ARGS+=("--max-turns" "${{ inputs.max_turns }}")
|
||||
fi
|
||||
|
||||
# Add allowed tools (include GitHub tools by default)
|
||||
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_comment,Read,Write,Edit,Bash"
|
||||
if [[ -n "${{ inputs.allowed_tools }}" ]]; then
|
||||
TOOLS="$TOOLS,${{ inputs.allowed_tools }}"
|
||||
fi
|
||||
CMD_ARGS+=("--allowedTools" "$TOOLS")
|
||||
|
||||
# Add disallowed tools
|
||||
if [[ -n "${{ inputs.disallowed_tools }}" ]]; then
|
||||
CMD_ARGS+=("--disallowedTools" "${{ inputs.disallowed_tools }}")
|
||||
fi
|
||||
|
||||
echo "📝 Executing Claude Code with prompt from file..."
|
||||
|
||||
# Execute Claude Code with timeout, using stdin for the prompt
|
||||
TIMEOUT_SECONDS=$((${{ inputs.timeout_minutes }} * 60))
|
||||
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "${{ env.PROMPT_FILE }}"
|
||||
|
||||
echo "✅ Claude Code execution completed"
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
@@ -0,0 +1,91 @@
|
||||
name: "Claude Issue Triage Action"
|
||||
description: "Automatically triage GitHub issues using Claude Code"
|
||||
|
||||
inputs:
|
||||
timeout_minutes:
|
||||
description: "Timeout in minutes for execution"
|
||||
required: false
|
||||
default: "5"
|
||||
claude_code_oauth_token:
|
||||
description: "Claude Code OAuth token for Pro subscription access"
|
||||
required: true
|
||||
github_token:
|
||||
description: "GitHub token with repo and issues permissions"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create prompt
|
||||
id: generate_prompt
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p /tmp/claude-prompts
|
||||
cat > /tmp/claude-prompts/claude-issue-triage-prompt.txt << 'EOF'
|
||||
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
|
||||
|
||||
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
|
||||
|
||||
Issue Information:
|
||||
- REPO: ${{ github.repository }}
|
||||
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
||||
TASK OVERVIEW:
|
||||
|
||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
||||
|
||||
2. Next, use the GitHub tools to get context about the issue:
|
||||
- You have access to these tools:
|
||||
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
|
||||
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
|
||||
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
|
||||
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
|
||||
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
|
||||
- Start by using mcp__github__get_issue to get the issue details
|
||||
|
||||
3. Analyze the issue content, considering:
|
||||
- The issue title and description
|
||||
- The type of issue (bug report, feature request, question, etc.)
|
||||
- Technical areas mentioned
|
||||
- Severity or priority indicators
|
||||
- User impact
|
||||
- Components affected
|
||||
|
||||
4. Select appropriate labels from the available labels list provided above:
|
||||
- Choose labels that accurately reflect the issue's nature
|
||||
- Be specific but comprehensive
|
||||
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
||||
- Consider platform labels (android, ios) if applicable
|
||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
||||
|
||||
5. Apply the selected labels:
|
||||
- Use mcp__github__update_issue to apply your selected labels
|
||||
- DO NOT post any comments explaining your decision
|
||||
- DO NOT communicate directly with users
|
||||
- If no labels are clearly applicable, do not apply any labels
|
||||
|
||||
IMPORTANT GUIDELINES:
|
||||
- Be thorough in your analysis
|
||||
- Only select labels from the provided list above
|
||||
- DO NOT post any comments to the issue
|
||||
- Your ONLY action should be to apply labels using mcp__github__update_issue
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
EOF
|
||||
|
||||
echo "PROMPT<<DELIMITER" >> $GITHUB_OUTPUT
|
||||
cat /tmp/claude-prompts/claude-issue-triage-prompt.txt >> $GITHUB_OUTPUT
|
||||
echo "DELIMITER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run Claude Code
|
||||
uses: ./.github/actions/claude-code-action
|
||||
with:
|
||||
direct_prompt: ${{ steps.generate_prompt.outputs.PROMPT }}
|
||||
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
||||
timeout_minutes: ${{ inputs.timeout_minutes }}
|
||||
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
|
||||
github_token: ${{ inputs.github_token }}
|
||||
@@ -251,7 +251,11 @@ jobs:
|
||||
});
|
||||
|
||||
- name: Stash merge gate helpers
|
||||
run: cp .github/scripts/merge-gate.js /tmp/merge-gate.js
|
||||
run: |
|
||||
# Keep scripts together so require('./gate-config.js') works from /tmp
|
||||
mkdir -p /tmp/merge-gate-scripts /tmp/gh-actions
|
||||
cp .github/scripts/merge-gate.js .github/scripts/gate-config.js /tmp/merge-gate-scripts/
|
||||
cp -R .github/actions/claude-code-action /tmp/gh-actions/
|
||||
|
||||
- name: Fetch PR branch
|
||||
env:
|
||||
@@ -259,6 +263,9 @@ jobs:
|
||||
run: |
|
||||
git fetch origin "pull/${PR_NUMBER}/head:pr-${PR_NUMBER}"
|
||||
git checkout "pr-${PR_NUMBER}"
|
||||
mkdir -p .github/actions
|
||||
rm -rf .github/actions/claude-code-action
|
||||
cp -R /tmp/gh-actions/claude-code-action .github/actions/
|
||||
|
||||
- name: Claude merge gate assessment (read-only)
|
||||
id: assess
|
||||
@@ -315,7 +322,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const mergeGate = require('/tmp/merge-gate.js');
|
||||
const mergeGate = require('/tmp/merge-gate-scripts/merge-gate.js');
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = ${{ matrix.pr_number }};
|
||||
@@ -354,7 +361,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const mergeGate = require('/tmp/merge-gate.js');
|
||||
const mergeGate = require('/tmp/merge-gate-scripts/merge-gate.js');
|
||||
const comments = await mergeGate.listAllComments(
|
||||
github, context.repo.owner, context.repo.repo, ${{ matrix.pr_number }}
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aiui"
|
||||
version = "0.3.127"
|
||||
version = "0.3.128"
|
||||
description = "YAML-driven website generator - CLI and compiler"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Version information."""
|
||||
|
||||
__version__ = "0.3.124"
|
||||
__version__ = "0.3.128"
|
||||
@@ -480,6 +480,9 @@ const DASHBOARD_STYLE = `
|
||||
@keyframes db-spin { to { transform: rotate(360deg); } }
|
||||
.db-spinner { display: inline-block; width: 24px; height: 24px; border: 2px solid var(--db-border); border-top-color: var(--db-accent); border-radius: 50%; animation: db-spin 0.8s linear infinite; }
|
||||
|
||||
/* Init error banner */
|
||||
.db-error { margin: 24px; padding: 16px 20px; border: 1px solid #b91c1c; border-radius: 8px; background: rgba(185, 28, 28, 0.12); color: #fca5a5; font-size: 14px; }
|
||||
|
||||
/* Hide React docs layout when dashboard is active */
|
||||
.db-active .sidebar:not(.db-sidebar), .db-active .topnav, .db-active .toc-sidebar,
|
||||
.db-active > .main-content, .db-active > nav:not(.db-sidebar) { display: none !important; }
|
||||
@@ -1495,6 +1498,20 @@ function buildSidebar(pages) {
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
function showDashboardInitError(message) {
|
||||
const root = document.getElementById('root');
|
||||
if (!root) return;
|
||||
let banner = document.getElementById('db-init-error');
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'db-init-error';
|
||||
banner.className = 'db-error';
|
||||
banner.setAttribute('role', 'alert');
|
||||
root.appendChild(banner);
|
||||
}
|
||||
banner.textContent = message;
|
||||
}
|
||||
|
||||
async function selectPage(pageId) {
|
||||
// Cleanup previous view if it has a cleanup function
|
||||
if (_activeCleanup) { try { _activeCleanup(); } catch(e) {} _activeCleanup = null; }
|
||||
@@ -1513,7 +1530,15 @@ async function selectPage(pageId) {
|
||||
|
||||
const page = pagesData.find(p => p.id === pageId);
|
||||
const main = document.getElementById('db-main-content');
|
||||
if (!main || !page) return;
|
||||
if (!main) {
|
||||
console.error('[AIUI] selectPage aborted: #db-main-content not in DOM — dashboard shell incomplete');
|
||||
showDashboardInitError('Dashboard shell layout incomplete. Reload the page or check the dashboard plugin.');
|
||||
return;
|
||||
}
|
||||
if (!page) {
|
||||
console.error(`[AIUI] selectPage aborted: page '${pageId}' not found in pagesData`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set page header (controlled by dashboard config protocol)
|
||||
const showPageHeader = main.dataset.showPageHeader !== 'false';
|
||||
|
||||
@@ -12,9 +12,9 @@ def test_feature_registry_basic():
|
||||
assert registry.is_implemented("site")
|
||||
assert registry.is_implemented("seo")
|
||||
assert registry.is_implemented("a11y")
|
||||
|
||||
# Test experimental features
|
||||
assert registry.is_experimental("i18n")
|
||||
# i18n graduated to implemented (since 0.5.0)
|
||||
assert registry.is_implemented("i18n")
|
||||
assert not registry.is_experimental("i18n")
|
||||
|
||||
# Test non-existent features
|
||||
assert not registry.is_implemented("nonexistent")
|
||||
@@ -34,14 +34,14 @@ def test_feature_registry_with_config():
|
||||
experimental = registry.get_experimental_fields(config)
|
||||
assert experimental == []
|
||||
|
||||
# Config with experimental features
|
||||
config_with_experimental = Config(
|
||||
# i18n is now an implemented feature, so it is not reported as experimental
|
||||
config_with_i18n = Config(
|
||||
site=SiteConfig(title="Test"),
|
||||
i18n=I18nConfig(locales=["en", "es"])
|
||||
)
|
||||
|
||||
experimental = registry.get_experimental_fields(config_with_experimental)
|
||||
assert "i18n" in experimental
|
||||
experimental = registry.get_experimental_fields(config_with_i18n)
|
||||
assert "i18n" not in experimental
|
||||
|
||||
|
||||
def test_feature_info():
|
||||
@@ -55,5 +55,5 @@ def test_feature_info():
|
||||
|
||||
i18n_info = registry.get_feature("i18n")
|
||||
assert i18n_info is not None
|
||||
assert i18n_info.status == FeatureStatus.EXPERIMENTAL
|
||||
assert i18n_info.since is None
|
||||
assert i18n_info.status == FeatureStatus.IMPLEMENTED
|
||||
assert i18n_info.since == "0.5.0"
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Regression tests for #175 — selectPage() silent abort when #db-main-content missing.
|
||||
|
||||
The dashboard style (`dashboard.js`) skips React. When the shell layout is
|
||||
incomplete and `#db-main-content` is absent, `selectPage()` used to return
|
||||
silently, leaving every view empty with no console error or user-visible signal.
|
||||
These source-parsing tests assert the defensive guard and error surface are
|
||||
wired in, mirroring `test_session_search_dashboard.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
FRONTEND = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "src" / "praisonaiui" / "templates" / "frontend" / "plugins"
|
||||
)
|
||||
DASHBOARD_JS = FRONTEND / "dashboard.js"
|
||||
|
||||
|
||||
class TestSelectPageGuard:
|
||||
def _select_page_body(self) -> str:
|
||||
src = DASHBOARD_JS.read_text()
|
||||
m = re.search(
|
||||
r"async function selectPage\(pageId\)\s*\{(.*?)^}",
|
||||
src, re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
assert m, "selectPage not found"
|
||||
return m.group(1)
|
||||
|
||||
def test_missing_main_logs_console_error(self):
|
||||
body = self._select_page_body()
|
||||
m = re.search(
|
||||
r"if \(!main\)\s*\{(.*?)\}", body, re.DOTALL,
|
||||
)
|
||||
assert m, "guard for missing #db-main-content not found"
|
||||
guard = m.group(1)
|
||||
assert "console.error" in guard
|
||||
assert "db-main-content" in guard
|
||||
|
||||
def test_missing_main_surfaces_init_error(self):
|
||||
body = self._select_page_body()
|
||||
m = re.search(
|
||||
r"if \(!main\)\s*\{(.*?)\}", body, re.DOTALL,
|
||||
)
|
||||
assert m, "guard for missing #db-main-content not found"
|
||||
assert "showDashboardInitError" in m.group(1)
|
||||
|
||||
def test_missing_page_logs_console_error(self):
|
||||
body = self._select_page_body()
|
||||
m = re.search(
|
||||
r"if \(!page\)\s*\{(.*?)\}", body, re.DOTALL,
|
||||
)
|
||||
assert m, "guard for missing page not found"
|
||||
assert "console.error" in m.group(1)
|
||||
|
||||
def test_show_dashboard_init_error_helper_present(self):
|
||||
src = DASHBOARD_JS.read_text()
|
||||
m = re.search(
|
||||
r"function showDashboardInitError\(message\)\s*\{(.*?)^}",
|
||||
src, re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
assert m, "showDashboardInitError helper not found"
|
||||
helper = m.group(1)
|
||||
assert "db-init-error" in helper
|
||||
assert "role" in helper and "alert" in helper
|
||||
|
||||
def test_no_silent_combined_return(self):
|
||||
body = self._select_page_body()
|
||||
assert "if (!main || !page) return;" not in body
|
||||
@@ -30,8 +30,15 @@ def test_jobs_submit_and_get(simple_jobs_client):
|
||||
assert detail.json()["id"] == job_id
|
||||
|
||||
|
||||
def _praisonai_jobs_available() -> bool:
|
||||
try:
|
||||
return __import__("importlib").util.find_spec("praisonai.jobs") is not None
|
||||
except ModuleNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
__import__("importlib").util.find_spec("praisonai.jobs") is None,
|
||||
not _praisonai_jobs_available(),
|
||||
reason="praisonai package not installed",
|
||||
)
|
||||
def test_sdk_job_store_roundtrip():
|
||||
|
||||
@@ -25,18 +25,25 @@ class TestDashboardSessionSearch:
|
||||
return DASHBOARD_JS.read_text()
|
||||
|
||||
def test_ctrl_k_keydown_handler_present(self):
|
||||
# The Ctrl+K binding is owned by the command palette, which exposes a
|
||||
# "Search sessions…" entry that routes to openSessionSearch(). Assert the
|
||||
# UX contract (Ctrl+K reaches session search) rather than where the
|
||||
# keydown listener physically lives.
|
||||
src = self._src()
|
||||
assert "initSessionSearch" in src
|
||||
m = re.search(
|
||||
r"function initSessionSearch\(\)\s*\{(.*?)^}",
|
||||
r"function initCommandPalette\(\)\s*\{(.*?)^}",
|
||||
src, re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
assert m, "initSessionSearch not found"
|
||||
assert m, "initCommandPalette not found"
|
||||
body = m.group(1)
|
||||
assert "keydown" in body
|
||||
assert "ctrlKey" in body and "metaKey" in body
|
||||
assert "'k'" in body or '"k"' in body
|
||||
assert "preventDefault" in body
|
||||
# The palette must route the "sessions" action to openSessionSearch().
|
||||
assert "openSessionSearch()" in src
|
||||
assert "action: 'sessions'" in src or 'action: "sessions"' in src
|
||||
|
||||
def test_palette_fetches_sessions_endpoint(self):
|
||||
src = self._src()
|
||||
|
||||
@@ -207,7 +207,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiui"
|
||||
version = "0.3.127"
|
||||
version = "0.3.128"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
@@ -1745,7 +1745,7 @@ name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
@@ -3352,11 +3352,11 @@ resolution-markers = [
|
||||
"python_full_version < '3.11' and sys_platform == 'win32'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "click", marker = "sys_platform == 'win32'" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
|
||||
{ name = "onnxruntime", marker = "sys_platform == 'win32'" },
|
||||
{ name = "python-dotenv", marker = "sys_platform == 'win32'" },
|
||||
{ name = "click" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "onnxruntime" },
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/8fdd991142ad3e037179a494b153f463024e5a211ef3ad948b955c26b4de/magika-0.6.2.tar.gz", hash = "sha256:37eb6ae8020f6e68f231bc06052c0a0cbe8e6fa27492db345e8dc867dbceb067", size = 3036634, upload-time = "2025-05-02T14:54:18.88Z" }
|
||||
wheels = [
|
||||
@@ -3376,11 +3376,11 @@ resolution-markers = [
|
||||
"python_full_version < '3.11' and sys_platform != 'win32'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "click", marker = "sys_platform != 'win32'" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'win32'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'win32'" },
|
||||
{ name = "onnxruntime", marker = "sys_platform != 'win32'" },
|
||||
{ name = "python-dotenv", marker = "sys_platform != 'win32'" },
|
||||
{ name = "click" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "onnxruntime" },
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/f3/3d1dcdd7b9c41d589f5cff252d32ed91cdf86ba84391cfc81d9d8773571d/magika-0.6.3.tar.gz", hash = "sha256:7cc52aa7359af861957043e2bf7265ed4741067251c104532765cd668c0c0cb1", size = 3042784, upload-time = "2025-10-30T15:22:34.499Z" }
|
||||
wheels = [
|
||||
@@ -6416,7 +6416,7 @@ resolution-markers = [
|
||||
"python_full_version < '3.11' and sys_platform != 'win32'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
|
||||
wheels = [
|
||||
@@ -6482,7 +6482,7 @@ resolution-markers = [
|
||||
"python_full_version == '3.11.*' and sys_platform != 'win32'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" } },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
||||
wheels = [
|
||||
@@ -6814,8 +6814,8 @@ name = "standard-aifc"
|
||||
version = "3.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "audioop-lts", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "standard-chunk", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "audioop-lts" },
|
||||
{ name = "standard-chunk" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" }
|
||||
wheels = [
|
||||
|
||||
Reference in new issue
Block a user