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
22 changes: 7 additions & 15 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
#!/usr/bin/env bash
set -e
# Pre-commit hook — run ruff linter before allowing a commit.
# Installed via core.hooksPath in .gitattributes. If you skip lint with
# `git commit --no-verify`, CI will catch it instead.

# Get staged Python files (excluding deleted files)
STAGED=$(git diff --cached --name-only --diff-filter=d -- '*.py')
set -euo pipefail

if [ -z "$STAGED" ]; then
exit 0
fi

# Fix lint issues and format staged files. The uv project lives under humux/
# (monorepo layout), so point uv at it; paths stay repo-root-relative and ruff
# discovers per-file config by walking up to humux/pyproject.toml.
uv run --project humux ruff check --fix $STAGED
uv run --project humux ruff format $STAGED

# Re-stage any files that were modified by ruff
git add $STAGED
echo "--- ruff check ---"
uv run --directory humux ruff check .
echo "OK"
118 changes: 85 additions & 33 deletions humux/api/templates/partials/memory_long_term.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,51 +19,103 @@
<th class="w-24">Subject</th>
<th>Content</th>
<th class="w-24">Scope</th>
<th class="w-10"></th>
</tr>
</thead>
<tbody>
{% for m in lt %}
<tr x-data="{editing:false}">
<tr x-data="{showDetail:false, modalEditing:false}"
@click="showDetail=true" class="cursor-pointer">
<td class="text-xs">{{ m.id }}</td>
<td class="text-xs">
<span x-show="!editing">{{ m.category }}</span>
<select name="category" class="input-sm" x-show="editing" aria-label="Category">
{% if m.category not in categories %}<option value="{{ m.category }}" selected>{{ m.category }}</option>{% endif %}
{% for c in categories %}<option value="{{ c }}" {% if m.category == c %}selected{% endif %}>{{ c }}</option>{% endfor %}
</select>
</td>
<td class="text-xs">
<span x-show="!editing">{{ m.subject }}</span>
<input name="subject" class="input-sm" value="{{ m.subject }}" x-show="editing" aria-label="Subject">
</td>
<td class="text-xs break-all">
<span x-show="!editing">{{ m.content }}</span>
<textarea name="content" class="input-sm w-full" rows="2" x-show="editing" aria-label="Content">{{ m.content }}</textarea>
<td class="text-xs">{{ m.category }}</td>
<td class="text-xs">{{ m.subject }}</td>
<td class="text-xs truncate max-w-0">
<span class="truncate block">{{ m.content }}</span>
</td>
<td class="text-xs">
<span x-show="!editing">{% if m.scope %}{{ m.scope }}{% else %}<span class="text-muted">shared</span>{% endif %}</span>
<select name="scope" class="input-sm" x-show="editing" aria-label="Scope">
<option value="" {% if not m.scope %}selected{% endif %}>shared</option>
{% if m.scope and m.scope not in agent_slugs %}<option value="{{ m.scope }}" selected>{{ m.scope }} (no agent)</option>{% endif %}
{% for s in agent_slugs %}<option value="{{ s }}" {% if m.scope == s %}selected{% endif %}>{{ s }}</option>{% endfor %}
</select>
{% if m.scope %}{{ m.scope }}{% else %}<span class="text-muted">shared</span>{% endif %}
</td>
<td class="whitespace-nowrap">
<button class="btn btn-sm" x-show="!editing" @click="editing=true">edit</button>
<button class="btn-primary btn-sm" x-show="editing"
hx-post="/memory/update" hx-target="#memory-sub" hx-swap="innerHTML"
hx-include="closest tr"
hx-vals='{"tier": "long-term", "memory_id": "{{ m.id }}"}'>&#x2713;</button>
<button class="btn btn-sm" x-show="editing" @click="editing=false">cancel</button>
<button class="btn-danger btn-sm" x-show="!editing"
hx-post="/memory/delete" hx-target="#memory-sub" hx-swap="innerHTML"
hx-vals='{"tier": "long-term", "memory_id": "{{ m.id }}"}' hx-confirm="Delete this memory?">x</button>
{# Invisible cell holding the modal #}
<td class="p-0 border-0" style="width:0">
<div x-show="showDetail"
class="fixed inset-0 z-50 flex items-center justify-center"
style="background:rgba(0,0,0,0.65);"
x-cloak
@keydown.window.escape="showDetail=false; modalEditing=false"
@click="showDetail=false; modalEditing=false">
<div class="card mx-4" style="max-width:440px;width:100%;max-height:80vh;overflow-y:auto;"
@click.stop
@click.outside="showDetail=false; modalEditing=false">
<div class="flex items-start justify-between mb-4 gap-3">
<span class="text-sm font-semibold">Memory #{{ m.id }}</span>
<button class="btn btn-sm shrink-0" @click.stop="showDetail=false; modalEditing=false">Close</button>
</div>

{# View mode #}
<dl x-show="!modalEditing" class="text-xs space-y-3">
<div>
<dt class="text-muted uppercase tracking-wider text-[10px] mb-0.5">Category</dt>
<dd>{{ m.category }}</dd>
</div>
<div>
<dt class="text-muted uppercase tracking-wider text-[10px] mb-0.5">Subject</dt>
<dd>{{ m.subject }}</dd>
</div>
<div>
<dt class="text-muted uppercase tracking-wider text-[10px] mb-0.5">Content</dt>
<dd class="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">{{ m.content }}</dd>
</div>
<div>
<dt class="text-muted uppercase tracking-wider text-[10px] mb-0.5">Scope</dt>
<dd>{% if m.scope %}{{ m.scope }}{% else %}<span class="text-muted">shared</span>{% endif %}</dd>
</div>
</dl>

{# Edit mode #}
<div x-show="modalEditing" class="space-y-3 text-xs">
<div>
<label class="text-muted uppercase tracking-wider text-[10px] mb-0.5 block">Category</label>
<select name="category" class="input-sm w-full" aria-label="Category">
{% if m.category not in categories %}<option value="{{ m.category }}" selected>{{ m.category }}</option>{% endif %}
{% for c in categories %}<option value="{{ c }}" {% if m.category == c %}selected{% endif %}>{{ c }}</option>{% endfor %}
</select>
</div>
<div>
<label class="text-muted uppercase tracking-wider text-[10px] mb-0.5 block">Subject</label>
<input name="subject" class="input-sm w-full" value="{{ m.subject }}" aria-label="Subject">
</div>
<div>
<label class="text-muted uppercase tracking-wider text-[10px] mb-0.5 block">Content</label>
<textarea name="content" class="input-sm w-full" rows="4" aria-label="Content">{{ m.content }}</textarea>
</div>
<div>
<label class="text-muted uppercase tracking-wider text-[10px] mb-0.5 block">Scope</label>
<select name="scope" class="input-sm w-full" aria-label="Scope">
<option value="" {% if not m.scope %}selected{% endif %}>shared</option>
{% if m.scope and m.scope not in agent_slugs %}<option value="{{ m.scope }}" selected>{{ m.scope }} (no agent)</option>{% endif %}
{% for s in agent_slugs %}<option value="{{ s }}" {% if m.scope == s %}selected{% endif %}>{{ s }}</option>{% endfor %}
</select>
</div>
</div>

{# Modal actions #}
<div class="flex items-center gap-2 mt-4 border-t border-border pt-3">
<button class="btn btn-sm" x-show="!modalEditing" @click.stop="modalEditing=true">Edit</button>
<button class="btn-primary btn-sm" x-show="modalEditing"
hx-post="/memory/update" hx-target="#memory-sub" hx-swap="innerHTML"
hx-include="closest td"
hx-vals='{"tier": "long-term", "memory_id": "{{ m.id }}"}'>&#x2713; Save</button>
<button class="btn btn-sm" x-show="modalEditing" @click.stop="modalEditing=false">Cancel</button>
<button class="btn-danger btn-sm ml-auto"
hx-post="/memory/delete" hx-target="#memory-sub" hx-swap="innerHTML"
hx-vals='{"tier": "long-term", "memory_id": "{{ m.id }}"}' hx-confirm="Delete this memory?">Delete</button>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
{% if not lt %}
<tr><td colspan="6" class="text-muted text-xs text-center py-4">No long-term memories</td></tr>
<tr><td colspan="5" class="text-muted text-xs text-center py-4">No long-term memories</td></tr>
{% endif %}
</tbody>
</table>
Expand Down
101 changes: 74 additions & 27 deletions humux/api/templates/partials/memory_short_term.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,92 @@
<th>Content</th>
<th class="w-24">Scope</th>
<th class="w-32">Expires</th>
<th class="w-10"></th>
</tr>
</thead>
<tbody>
{% for m in st %}
<tr x-data="{editing:false}">
<tr x-data="{showDetail:false, modalEditing:false}"
@click="showDetail=true" class="cursor-pointer">
<td class="text-xs">{{ m.id }}</td>
<td class="text-xs break-all">
<span x-show="!editing">{{ m.content }}</span>
<input name="content" class="input-sm w-full" value="{{ m.content }}" x-show="editing" aria-label="Content">
<td class="text-xs truncate max-w-0">
<span class="truncate block">{{ m.content }}</span>
</td>
<td class="text-xs">
<span x-show="!editing">{% if m.scope %}{{ m.scope }}{% else %}<span class="text-muted">shared</span>{% endif %}</span>
<select name="scope" class="input-sm" x-show="editing" aria-label="Scope">
<option value="" {% if not m.scope %}selected{% endif %}>shared</option>
{% if m.scope and m.scope not in agent_slugs %}<option value="{{ m.scope }}" selected>{{ m.scope }} (no agent)</option>{% endif %}
{% for s in agent_slugs %}<option value="{{ s }}" {% if m.scope == s %}selected{% endif %}>{{ s }}</option>{% endfor %}
</select>
{% if m.scope %}{{ m.scope }}{% else %}<span class="text-muted">shared</span>{% endif %}
</td>
<td class="text-xs">
<span x-show="!editing">{{ m.expires_at }}</span>
<input type="datetime-local" name="expires_at" step="1" class="input-sm"
value="{{ m.expires_at[:19]|replace(' ','T') }}" x-show="editing" aria-label="Expires at">
</td>
<td class="whitespace-nowrap">
<button class="btn btn-sm" x-show="!editing" @click="editing=true">edit</button>
<button class="btn-primary btn-sm" x-show="editing"
hx-post="/memory/update" hx-target="#memory-sub" hx-swap="innerHTML"
hx-include="closest tr"
hx-vals='{"tier": "short-term", "memory_id": "{{ m.id }}"}'>&#x2713;</button>
<button class="btn btn-sm" x-show="editing" @click="editing=false">cancel</button>
<button class="btn-danger btn-sm" x-show="!editing"
hx-post="/memory/delete" hx-target="#memory-sub" hx-swap="innerHTML"
hx-vals='{"tier": "short-term", "memory_id": "{{ m.id }}"}' hx-confirm="Delete this memory?">x</button>
<td class="text-xs">{{ m.expires_at }}</td>
{# Invisible cell holding the modal #}
<td class="p-0 border-0" style="width:0">
<div x-show="showDetail"
class="fixed inset-0 z-50 flex items-center justify-center"
style="background:rgba(0,0,0,0.65);"
x-cloak
@keydown.window.escape="showDetail=false; modalEditing=false"
@click="showDetail=false; modalEditing=false">
<div class="card mx-4" style="max-width:440px;width:100%;max-height:80vh;overflow-y:auto;"
@click.stop
@click.outside="showDetail=false; modalEditing=false">
<div class="flex items-start justify-between mb-4 gap-3">
<span class="text-sm font-semibold">Memory #{{ m.id }}</span>
<button class="btn btn-sm shrink-0" @click.stop="showDetail=false; modalEditing=false">Close</button>
</div>

{# View mode #}
<dl x-show="!modalEditing" class="text-xs space-y-3">
<div>
<dt class="text-muted uppercase tracking-wider text-[10px] mb-0.5">Content</dt>
<dd class="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">{{ m.content }}</dd>
</div>
<div>
<dt class="text-muted uppercase tracking-wider text-[10px] mb-0.5">Scope</dt>
<dd>{% if m.scope %}{{ m.scope }}{% else %}<span class="text-muted">shared</span>{% endif %}</dd>
</div>
<div>
<dt class="text-muted uppercase tracking-wider text-[10px] mb-0.5">Expires</dt>
<dd>{{ m.expires_at }}</dd>
</div>
</dl>

{# Edit mode #}
<div x-show="modalEditing" class="space-y-3 text-xs">
<div>
<label class="text-muted uppercase tracking-wider text-[10px] mb-0.5 block">Content</label>
<textarea name="content" class="input-sm w-full" rows="4" aria-label="Content">{{ m.content }}</textarea>
</div>
<div>
<label class="text-muted uppercase tracking-wider text-[10px] mb-0.5 block">Scope</label>
<select name="scope" class="input-sm w-full" aria-label="Scope">
<option value="" {% if not m.scope %}selected{% endif %}>shared</option>
{% if m.scope and m.scope not in agent_slugs %}<option value="{{ m.scope }}" selected>{{ m.scope }} (no agent)</option>{% endif %}
{% for s in agent_slugs %}<option value="{{ s }}" {% if m.scope == s %}selected{% endif %}>{{ s }}</option>{% endfor %}
</select>
</div>
<div>
<label class="text-muted uppercase tracking-wider text-[10px] mb-0.5 block">Expires</label>
<input type="datetime-local" name="expires_at" step="1" class="input-sm w-full"
value="{{ m.expires_at[:19]|replace(' ','T') }}" aria-label="Expires at">
</div>
</div>

{# Modal actions #}
<div class="flex items-center gap-2 mt-4 border-t border-border pt-3">
<button class="btn btn-sm" x-show="!modalEditing" @click.stop="modalEditing=true">Edit</button>
<button class="btn-primary btn-sm" x-show="modalEditing"
hx-post="/memory/update" hx-target="#memory-sub" hx-swap="innerHTML"
hx-include="closest td"
hx-vals='{"tier": "short-term", "memory_id": "{{ m.id }}"}'>&#x2713; Save</button>
<button class="btn btn-sm" x-show="modalEditing" @click.stop="modalEditing=false">Cancel</button>
<button class="btn-danger btn-sm ml-auto"
hx-post="/memory/delete" hx-target="#memory-sub" hx-swap="innerHTML"
hx-vals='{"tier": "short-term", "memory_id": "{{ m.id }}"}' hx-confirm="Delete this memory?">Delete</button>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
{% if not st %}
<tr><td colspan="5" class="text-muted text-xs text-center py-4">No active short-term memories</td></tr>
<tr><td colspan="4" class="text-muted text-xs text-center py-4">No active short-term memories</td></tr>
{% endif %}
</tbody>
</table>
Expand Down
Loading