Skip to content
Open
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
47 changes: 44 additions & 3 deletions .claude/agent/queue.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
{
"version": "1.0.0",
"tasks": [],
"completed": [],
"last_sync": null
"tasks": [
{
"id": "gh-1",
"type": "feature",
"priority": 4,
"spec": "GitHub Issue #1: Add markdown rendering for chat messages\n\n## Description\nAI responses often contain markdown (code blocks, lists, headers, etc.) but currently render as plain text.\n\n## Tasks\n- [ ] Install `marked` and `@tailwindcss/typography`\n- [ ] Update `ChatMessage.vue` to render markdown for assistant messages\n- [ ] Add proper styling for code blocks, lists, tables\n- [ ] Consider syntax highlighting for code blocks (highlight.js or similar)\n\n## Acceptance Criteria\n- Code blocks render with proper formatting\n- Lists, headers, bold/italic render correctly\n- User messages remain plain text\n- Dark mode supported\n\nClose with: fixes #1",
"status": "pending",
"github_issue": 1,
"created_at": "2024-12-08T17:50:00Z"
},
{
"id": "gh-3",
"type": "feature",
"priority": 1,
"spec": "GitHub Issue #3: Add model info and descriptions to model selector\n\n## Description\nHelp users understand which model to choose by showing descriptions, capabilities, and use cases.\n\n## Tasks\n- [ ] Enhance model selector dropdown with descriptions\n- [ ] Add model capability badges (fast, creative, coding, etc.)\n- [ ] Show model size/context length info\n- [ ] Consider tooltip or expandable info section\n- [ ] Update ModelName enum with richer metadata\n\n## Model info to display\n- **Name**: Human-readable name\n- **Description**: 1-2 sentence summary\n- **Best for**: Use cases (coding, creative writing, analysis)\n- **Speed**: Fast/Medium/Slow indicator\n- **Context length**: Token limit\n\n## Acceptance Criteria\n- Users can make informed model choices\n- Info doesn't clutter the UI\n- Works on mobile\n\nClose with: fixes #3",
"status": "pending",
"github_issue": 3,
"created_at": "2024-12-08T17:50:00Z"
}
],
"completed": [
{
"id": "gh-21",
"type": "feature",
"priority": 2,
"spec": "GitHub Issue #21: Add Agent Builder following IndyDevDan's standards",
"status": "complete",
"started_at": "2025-12-08T00:00:00Z",
"completed_at": "2025-12-08T01:00:00Z",
"github_issue": 21,
"created_at": "2024-12-08T17:50:00Z",
"output": "Implemented Agent Builder with full CRUD UI and chat system integration.\n\n## Implementation Summary:\nThe Agent Builder feature was already implemented with:\n- Agent model with proper Eloquent relationships (user, defaultModel, models)\n- Full CRUD controller (AgentController)\n- Vue UI pages (Index, Create, Edit, Show)\n- Comprehensive test coverage (43 tests for agents)\n\n## New Integration Work:\nAdded chat system integration to use custom agents:\n\n### Backend Changes:\n- app/Models/Chat.php: Added agent_id fillable and agent() relationship\n- app/Http/Controllers/ChatController.php: Added agents to index/show views, support for agent_id in store\n- app/Http/Requests/StoreChatRequest.php: Added agent_id validation (nullable, exists)\n- app/Http/Requests/UpdateChatRequest.php: Added agent_id validation (sometimes, nullable)\n\n### Frontend Changes:\n- resources/js/types/chat.ts: Added agent_id and agent to Chat interface\n- resources/js/pages/Chat/Index.vue: Added agent selector dropdown when creating new chat\n- resources/js/pages/Chat/Show.vue: Added agent selector in header, agent indicator badge\n\n## Tests:\n- All 362 tests pass (844 assertions)\n- Existing Agent tests (43) + Chat tests (95) all passing",
"files_changed": [
"app/Models/Chat.php",
"app/Http/Controllers/ChatController.php",
"app/Http/Requests/StoreChatRequest.php",
"app/Http/Requests/UpdateChatRequest.php",
"resources/js/types/chat.ts",
"resources/js/pages/Chat/Index.vue",
"resources/js/pages/Chat/Show.vue"
]
}
],
"last_sync": "2024-12-08T17:50:00Z"
}
112 changes: 111 additions & 1 deletion app/Http/Controllers/AgentController.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,118 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\StoreAgentRequest;
use App\Http\Requests\UpdateAgentRequest;
use App\Models\Agent;
use App\Models\AiModel;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;

class AgentController extends Controller
{
//
public function index(Request $request): Response
{
$agents = Agent::query()
->where(function ($query) use ($request) {
$query->whereNull('user_id')
->orWhere('user_id', $request->user()->id);
})
->with('defaultModel')
->orderByDesc('updated_at')
->get();

return Inertia::render('Agents/Index', [
'agents' => $agents,
]);
}

public function create(): Response
{
return Inertia::render('Agents/Create', [
'models' => AiModel::query()->enabled()->get(),
'availableTools' => $this->getAvailableTools(),
'availableCapabilities' => $this->getAvailableCapabilities(),
]);
}

public function store(StoreAgentRequest $request): RedirectResponse
{
$agent = $request->user()->agents()->create($request->validated());

return to_route('agents.show', $agent);
}

public function show(Request $request, Agent $agent): Response
{
abort_unless(
$agent->user_id === null || $agent->user_id === $request->user()->id,
403
);

return Inertia::render('Agents/Show', [
'agent' => $agent->load('defaultModel'),
'models' => AiModel::query()->enabled()->get(),
'availableTools' => $this->getAvailableTools(),
'availableCapabilities' => $this->getAvailableCapabilities(),
]);
}

public function edit(Request $request, Agent $agent): Response
{
abort_unless($agent->user_id === $request->user()->id, 403);

return Inertia::render('Agents/Edit', [
'agent' => $agent->load('defaultModel'),
'models' => AiModel::query()->enabled()->get(),
'availableTools' => $this->getAvailableTools(),
'availableCapabilities' => $this->getAvailableCapabilities(),
]);
}

public function update(UpdateAgentRequest $request, Agent $agent): RedirectResponse
{
$agent->update($request->validated());

return to_route('agents.show', $agent);
}

public function destroy(Request $request, Agent $agent): RedirectResponse
{
abort_unless($agent->user_id === $request->user()->id, 403);

$agent->delete();

return to_route('agents.index');
}

/**
* @return array<int, array{id: string, name: string, description: string}>
*/
private function getAvailableTools(): array
{
return [
['id' => 'web_search', 'name' => 'Web Search', 'description' => 'Search the web for information'],
['id' => 'code_interpreter', 'name' => 'Code Interpreter', 'description' => 'Execute and analyze code'],
['id' => 'file_reader', 'name' => 'File Reader', 'description' => 'Read and parse files'],
['id' => 'knowledge_base', 'name' => 'Knowledge Base', 'description' => 'Search internal knowledge base'],
];
}

/**
* @return array<int, array{id: string, name: string, description: string}>
*/
private function getAvailableCapabilities(): array
{
return [
['id' => 'reasoning', 'name' => 'Advanced Reasoning', 'description' => 'Complex problem solving'],
['id' => 'creativity', 'name' => 'Creative Writing', 'description' => 'Generate creative content'],
['id' => 'analysis', 'name' => 'Data Analysis', 'description' => 'Analyze and interpret data'],
['id' => 'coding', 'name' => 'Code Generation', 'description' => 'Write and review code'],
];
}
}
26 changes: 25 additions & 1 deletion app/Http/Controllers/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Http\Requests\StoreChatRequest;
use App\Http\Requests\UpdateChatRequest;
use App\Models\Agent;
use App\Models\Chat;
use App\Services\ModelSyncService;
use Illuminate\Http\RedirectResponse;
Expand All @@ -22,10 +23,12 @@ public function __construct(
public function index(Request $request): Response
{
$chats = $request->user()->chats()->orderByDesc('updated_at')->get();
$agents = $this->getAvailableAgents($request);

return Inertia::render('Chat/Index', [
'chats' => $chats,
'models' => $this->modelSyncService->syncAndGetAvailable(),
'agents' => $agents,
]);
}

Expand All @@ -34,6 +37,7 @@ public function store(StoreChatRequest $request): RedirectResponse
$chat = $request->user()->chats()->create([
'title' => str($request->validated('message'))->limit(50)->toString(),
'ai_model_id' => $request->validated('ai_model_id'),
'agent_id' => $request->validated('agent_id'),
]);

return to_route('chats.show', $chat);
Expand All @@ -44,11 +48,13 @@ public function show(Request $request, Chat $chat): Response
abort_unless($chat->user_id === $request->user()->id, 403);

$chats = $request->user()->chats()->orderByDesc('updated_at')->get();
$agents = $this->getAvailableAgents($request);

return Inertia::render('Chat/Show', [
'chat' => $chat->load('messages'),
'chat' => $chat->load(['messages', 'agent']),
'chats' => $chats,
'models' => $this->modelSyncService->syncAndGetAvailable(),
'agents' => $agents,
]);
}

Expand All @@ -67,4 +73,22 @@ public function destroy(Request $request, Chat $chat): RedirectResponse

return to_route('chats.index');
}

/**
* Get agents available to the current user.
*
* @return \Illuminate\Database\Eloquent\Collection<int, Agent>
*/
private function getAvailableAgents(Request $request): \Illuminate\Database\Eloquent\Collection
{
return Agent::query()
->where(function ($query) use ($request) {
$query->whereNull('user_id')
->orWhere('user_id', $request->user()->id);
})
->active()
->with('defaultModel')
->orderBy('name')
->get();
}
}
129 changes: 129 additions & 0 deletions app/Http/Controllers/GitHubAppController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace App\Http\Controllers;

use App\Services\GitHubAppService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class GitHubAppController extends Controller
{
public function __construct(
private GitHubAppService $gitHubApp
) {}

/**
* Show GitHub App status and setup page.
*/
public function index(): RedirectResponse
{
// TODO: Create GitHub/Index.vue page
return redirect()->route('dashboard')
->with('success', 'GitHub App connected!');
}

/**
* Start device flow authentication.
* Returns a user code to enter at github.com/login/device
*/
public function device(): JsonResponse
{
$deviceCode = $this->gitHubApp->startDeviceFlow();

return response()->json([
'device_code' => $deviceCode['device_code'],
'user_code' => $deviceCode['user_code'],
'verification_uri' => $deviceCode['verification_uri'],
'expires_in' => $deviceCode['expires_in'],
'interval' => $deviceCode['interval'],
]);
}

/**
* Poll for device flow completion.
*/
public function poll(Request $request): JsonResponse
{
$request->validate([
'device_code' => 'required|string',
]);

$result = $this->gitHubApp->pollDeviceFlow($request->device_code);

return response()->json($result);
}

/**
* OAuth callback (for web flow, if needed later).
*/
public function callback(Request $request): RedirectResponse
{
$code = $request->query('code');
$installationId = $request->query('installation_id');
$setupAction = $request->query('setup_action');

if ($installationId) {
$this->gitHubApp->handleInstallation($installationId);
}

if ($code) {
$this->gitHubApp->exchangeCodeForToken($code);
}

return redirect()->route('github.index')
->with('success', 'GitHub App configured successfully!');
}

/**
* Webhook endpoint (placeholder - needs tunnel for real use).
*/
public function webhook(Request $request): JsonResponse
{
$event = $request->header('X-GitHub-Event');
$payload = $request->all();

// Log for now, process later when we have tunnel
logger()->info('GitHub webhook received', [
'event' => $event,
'action' => $payload['action'] ?? null,
]);

return response()->json(['status' => 'received']);
}

/**
* Test making a commit as the app.
*/
public function testCommit(Request $request): JsonResponse
{
$request->validate([
'installation_id' => 'required|integer',
'repo' => 'required|string',
]);

$result = $this->gitHubApp->testCommitAsApp(
$request->installation_id,
$request->repo
);

return response()->json($result);
}

/**
* Get installation token for API calls.
*/
public function installationToken(Request $request): JsonResponse
{
$request->validate([
'installation_id' => 'required|integer',
]);

$token = $this->gitHubApp->getInstallationToken($request->installation_id);

return response()->json([
'token' => $token['token'],
'expires_at' => $token['expires_at'],
]);
}
}
1 change: 1 addition & 0 deletions app/Http/Requests/StoreChatRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function rules(): array
return [
'message' => ['required', 'string', 'max:10000'],
'ai_model_id' => ['required', 'integer', 'exists:ai_models,id'],
'agent_id' => ['nullable', 'integer', 'exists:agents,id'],
];
}
}
1 change: 1 addition & 0 deletions app/Http/Requests/UpdateChatRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function rules(): array
return [
'ai_model_id' => ['sometimes', 'integer', 'exists:ai_models,id'],
'title' => ['sometimes', 'string', 'max:255'],
'agent_id' => ['sometimes', 'nullable', 'integer', 'exists:agents,id'],
];
}
}
Loading