{# Vendor assets via asset() helper. Default mode = CDN (jsDelivr) with SRI; #} {# when `ntasker assets fetch` ran and `assets_mode=local|auto`, they come from #} {# the user-data dir under /static/vendor instead. SRI is emitted in both modes #} {# (local mode: tamper detection). `crossorigin` is only required for CDN loading. #} {% for tpl in plugin_slots['head'] %}{% include tpl %}{% endfor %} {# Frontend i18n bridge: js_strings is built server-side per request. #} {# Alpine magic property `$i18n('key')` is registered just below. #} {# Sidebar section heading: a button that folds the section's body. #} {# `key` is a name in settings.SIDEBAR_SECTIONS; the open state lives in #} {# `sectionOpen` and is persisted to the `sidebar_sections` setting. #} {% macro section_toggle(key, icon, label) %} {% endmacro %} {# One sidebar project row. `p` is the Alpine expression yielding the #} {# project ({name, label, open_count}); the same markup serves flat rows, #} {# family heads and indented children. `after_badge` is extra markup that #} {# follows the open-count badge (the folded-family pill on head rows). #} {% macro project_row(p, after_badge='') %} {% endmacro %} {# Action slots of a task row / board card -- the same three everywhere: the #} {# agent button (resume when the task already has a session, else run), edit, #} {# and a "..." menu with everything else. `compact` is the tighter board form. #} {% macro task_actions(compact=false) %} {% set b = 'btn btn-sm' ~ (' px-1 py-0' if compact else '') %} {# Run -- only when the agent CLI + a PTY are available. The session #} {# indicator (spinner / waiting) sits INSIDE the button, left of the #} {# logo -- one pill; clicking a live one opens the session. #} {# Agent CLI not installed: greyed-out logo in a non-clickable box, so #} {# you still see which agent the task uses and the slot keeps its width. #} {# Resume: reopen a finished task's stored session. #} {% endmacro %} {# Folded-family pill on a head row: how many children hide behind the #} {# chevron; lit up when one of them is part of the active filter. #} {# One row of the session pick-up dialog -- same markup for the running and the #} {# finished list. `row` is the Alpine loop variable of the enclosing template. #} {% macro session_pick_row() %}
{% endmacro %} {% macro fold_pill() %} {% endmacro %}
{# Sidebar: project + tag filter (full-width checkboxes). Hidden on mobile (<768px). #} {# Sticky-positioned aside outside the .page wrapper. Tabler/Bootstrap surface vars #} {# drive the bg so dark-mode behaves. Sidebar has its own scroll context; the main #} {# page-wrapper scrolls independently. #} {# Drag handle on the sidebar's edge: resizes the sidebar, width persists in localStorage. #}
{# Main content (full-width inside its column) #}
{# Tabler page-header: title left, view-mode switcher right. #} {# The two pseudo-tabs (Task list / Kanban) drive ``viewMode`` -- #} {# they are NOT bootstrap nav-tabs because the page wrappers below #} {# swap their entire layout, not just a tab-pane. #}
{# v2.0: projects_dir-Banner is gone -- projects emerge #} {# implicitly from tasks; no filesystem setup required. #} {# New-task form. The header doubles as an accordion toggle and #} {# the body starts collapsed, so more of the task list stays #} {# visible until the user actually wants to add something. #}

{{ t('New task') }}

{# The task itself comes first and unlabeled: the title is the one #} {# field every task needs, so it is the biggest thing in the form. #}
{# An image pasted here becomes a context attachment (task_context plugin). #}
{# Tools that act on the description (attach context, dictate) sit #} {# right under it in one row; each plugin slot is a flex child. #} {% if plugin_slots['task_form'] %}
{% for tpl in plugin_slots['task_form'] %}{% include tpl %}{% endfor %}
{% endif %}

{{ t('Project') }} {# Misc chip: one click puts the task into the misc project. #} {# Only rendered while the misc_project setting is set. #}
{# Free-form text + AlpineJS combobox: pick an existing project #} {# (Tab/Enter accept the highlighted suggestion, like tags) or #} {# type a new name -- created implicitly when the task is saved. #} {# Empty = cross-project. #}
{# Live match indicator: green check on an exact existing-project #} {# match, a "new folder" icon on a name that does not exist yet #} {# (it will be created on first run -- see message below). #}
{# New-project notice: shown the instant a name that does not exist #} {# yet is typed. Tells the user a directory will be created on run. #}
{# Model: free text with the agent's suggestions; empty = the agent's _model setting. #}
{# Tag-Input: AlpineJS-driven combobox. Tags as badges, free-text input, autocomplete dropdown. #}
{# Deps and locks are hidden for a misc task: it relates to no #} {# other project (createTask sends both empty). #}
{# Dependency input: autocomplete by title/#id, picks resolve to #id chips. #}
{# Directory-lock chips: extra projects whose directories this run holds. #} {# Project-name combobox (pick = chip) + drop target for sidebar projects. #}

{# Draft: a parked idea -- never started by the queue, the run button or /task. #} {# Sits next to the actions because it decides what "create" means. #} {# Fasttrack: the agent commits + finishes the task itself; the second #} {# switch lets a failed run leave the queue instead of blocking its lane. #} {# Create + immediately hand the task to its agent -- only when that agent's CLI + a PTY are available. #}
{# ===== Inbox ===== #} {# The triage's proposals (see ntasker.triage) stacked like the new-task form: #} {# answer the triage's question or correct it right on the card, then accept. #} {# Notes still being triaged (or failed) come first. Hidden while empty. #}
{{ t('Inbox') }}
{# Background catch-up of the project summaries (ntasker.triage.summary_worker): visible only while the catalog is incomplete, so the wait has a number. #}
{# ===== Task queue ===== #} {# The auto-run worklist -- and the only way a session starts. #} {# Every run button appends its task to this queue; #} {# ntasker works it through: one running task per project, next #} {# one up as soon as the previous session ends. Dragging inside #} {# the panel reorders it; the header button pauses / resumes. #} {# The rail down the left side marches while the queue runs and #} {# freezes when it is paused -- that is the state indicator. #} {# The whole panel is a drop target for a board card (appends it). #}
{# Run log: one entry per fasttrack run (see /api/outcomes). #} {# Git worktrees and their end decision (see /api/worktrees). #} {# Carry on with a conversation started in a terminal. #} {# The two queue-wide controls, pushed right as one block #} {# so they stay together whether or not the planner shows. #}
{# Queue planner: "Plan queue" turns on picking; the ticked #} {# tasks then go to an agent session that records their #} {# dependencies, sets the run order and switches the queue on. #} {# Solid only when paused with work waiting -- that is the #} {# one state where pressing it changes something visible. #}
{# The empty state says how tasks get in, so it stays visible. #}
{# One column per project -- the worker runs one task per #} {# project at a time, so a column is literally one execution #} {# lane. Cross-project first, then alphabetical; that column #} {# is no lane -- tasks without a project run side by side. #}
{# ===== Worktrees ===== #} {# Open git worktrees; a pending one (no open task left) waits for the end decision. #}
{# ===== Run log ===== #} {# Every fasttrack run leaves one outcome: ok (green), failed / blocked #} {# (with the agent's summary) or ended (session died without finish). #} {# Acknowledging deletes the entry; the report stays on the task. #}
{# Status switch -- list view only. Kanban renders its own column layout below. #}
{# Task list #}

{{ t('No tasks') }}

{# ===== Kanban board view ===== #} {# 4 columns: 3 phase columns (planned/wip/review) + 1 collapsible #} {# Done column derived from status='done'. Drag a card between #} {# columns -> PATCH phase (or status when crossing into Done). #} {# Same filters (search / sidebar) feed the same task list; #} {# the kanban view just groups it by phase. #}
{# === Claude run view -- full-page; replaces list/kanban while open. #} {# One tab per live session; the runs keep streaming in the background #} {# when you go Back. Each tab owns its own xterm + WebSocket. === #}
{# Header row 1: back + one tab per open session. #}
{# Tab strip: one per open session. Click switches (drives the #/run/ hash). #}
{# The active run's project and task as the page heading. #} {# Header row 2: the view switch (conversation / terminal / diff, plus #} {# the report as a side pane) on the left, the run's actions on the right. #}
{# Conversation pane: alone, or left of the terminal in split mode (see _conversation.html). #} {# Split mode: the terminal in a pane of its own, width persists in localStorage. #} {# One xterm host per tab; only the active one is shown. Stable #} {# :id lets _claudeConnect attach the Terminal to the right node. #} {# Diff page in place of the terminal (see _diff.html). #} {# Report pane: splitter + the rendered report, width persists in localStorage. #}
{# Terminal-only view: the quick prompts below the terminal, where the #} {# conversation pane has them above its reply box (see _quick_prompts.html). #}
{# Edit modal #} {# Diff modal -- what the task's run changed, opened from the task card. #} {# Session pick-up modal -- conversations started outside ntasker, listed from their transcripts. Each row can become (or join) a task and is then reopened right here. #} {# Worktree decision -- what happens to a finished git worktree: merge, keep or discard. #} {# Report modal -- the agent's final report, rendered Markdown, read-only. #} {# New-project modal -- directory + group (see _new_project.html). #} {% include '_new_project.html' %}
{% for tpl in plugin_slots['modals'] %}{% include tpl %}{% endfor %} {# Toast container (Tabler/Bootstrap-compatible). Toasts injected by app.js. #}
{# xterm.js for the interactive Claude terminal. Loaded eagerly (no defer) so #} {# the Terminal / FitAddon globals exist before app.js uses them at runtime. #} {# Markdown pipeline for the report modal (and the workspace plugin's file #} {# viewer): marked renders, DOMPurify sanitises. Loaded before app.js. #} {# Plugin scripts push their mixins before app.js builds the component. #} {% for tpl in plugin_slots['scripts'] %}{% include tpl %}{% endfor %}