feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
// ── Slash commands ──────────────────────────────────────────────────────────
|
|
|
|
|
// Built-in commands intercepted before send(). Each command runs locally
|
|
|
|
|
// (no round-trip to the agent) and shows feedback via toast or local message.
|
|
|
|
|
|
|
|
|
|
const COMMANDS=[
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
// noEcho:true = action-only commands that don't produce a chat response.
|
|
|
|
|
// Commands without noEcho get a user message echoed to the chat (#840).
|
2026-04-08 14:55:03 +00:00
|
|
|
{name:'help', desc:t('cmd_help'), fn:cmdHelp},
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
{name:'clear', desc:t('cmd_clear'), fn:cmdClear, noEcho:true},
|
|
|
|
|
{name:'compress', desc:t('cmd_compress'), fn:cmdCompress, arg:'[focus topic]', noEcho:true},
|
|
|
|
|
{name:'compact', desc:t('cmd_compact_alias'), fn:cmdCompact, noEcho:true},
|
|
|
|
|
{name:'model', desc:t('cmd_model'), fn:cmdModel, arg:'model_name', subArgs:'models', noEcho:true},
|
|
|
|
|
{name:'workspace', desc:t('cmd_workspace'), fn:cmdWorkspace, arg:'name', noEcho:true},
|
|
|
|
|
{name:'new', desc:t('cmd_new'), fn:cmdNew, noEcho:true},
|
|
|
|
|
{name:'usage', desc:t('cmd_usage'), fn:cmdUsage, noEcho:true},
|
|
|
|
|
{name:'theme', desc:t('cmd_theme'), fn:cmdTheme, arg:'name', noEcho:true},
|
2026-04-20 22:04:28 +00:00
|
|
|
{name:'personality', desc:t('cmd_personality'), fn:cmdPersonality, arg:'name', subArgs:'personalities'},
|
2026-04-19 05:37:44 +00:00
|
|
|
{name:'skills', desc:t('cmd_skills'), fn:cmdSkills, arg:'query'},
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
{name:'stop', desc:t('cmd_stop'), fn:cmdStop, noEcho:true},
|
2026-04-19 05:37:44 +00:00
|
|
|
{name:'title', desc:t('cmd_title'), fn:cmdTitle, arg:'[title]'},
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
{name:'retry', desc:t('cmd_retry'), fn:cmdRetry, noEcho:true},
|
|
|
|
|
{name:'undo', desc:t('cmd_undo'), fn:cmdUndo, noEcho:true},
|
2026-04-24 01:24:51 +00:00
|
|
|
{name:'btw', desc:t('cmd_btw'), fn:cmdBtw, arg:'question', noEcho:true},
|
|
|
|
|
{name:'background',desc:t('cmd_background'),fn:cmdBackground,arg:'prompt', noEcho:true},
|
2026-04-19 05:37:44 +00:00
|
|
|
{name:'status', desc:t('cmd_status'), fn:cmdStatus},
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
{name:'voice', desc:t('cmd_voice'), fn:cmdVoice, noEcho:true},
|
|
|
|
|
{name:'reasoning', desc:t('cmd_reasoning'), fn:cmdReasoning, arg:'show|hide|none|minimal|low|medium|high|xhigh', subArgs:['show','hide','none','minimal','low','medium','high','xhigh'], noEcho:true},
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
];
|
|
|
|
|
|
2026-04-20 22:04:28 +00:00
|
|
|
const SLASH_SUBARG_SOURCES={
|
|
|
|
|
model:{desc:t('cmd_model'), subArgs:'models'},
|
|
|
|
|
personality:{desc:t('cmd_personality'), subArgs:'personalities'},
|
|
|
|
|
};
|
|
|
|
|
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
function parseCommand(text){
|
|
|
|
|
if(!text.startsWith('/'))return null;
|
|
|
|
|
const parts=text.slice(1).split(/\s+/);
|
|
|
|
|
const name=parts[0].toLowerCase();
|
|
|
|
|
const args=parts.slice(1).join(' ').trim();
|
|
|
|
|
return {name,args};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function executeCommand(text){
|
|
|
|
|
const parsed=parseCommand(text);
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
if(!parsed)return null;
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
const cmd=COMMANDS.find(c=>c.name===parsed.name);
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
if(!cmd)return null;
|
2026-04-21 22:26:52 +00:00
|
|
|
// A handler may return `false` to opt out of interception — e.g. /reasoning
|
|
|
|
|
// with an effort level falls through so the agent's own handler sees it,
|
|
|
|
|
// preserving the pre-existing pass-through behaviour for that subcommand.
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
if(cmd.fn(parsed.args)===false)return null;
|
|
|
|
|
// Return noEcho flag so send() knows whether to echo the command as a user message (#840).
|
|
|
|
|
return {noEcho:!!cmd.noEcho};
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getMatchingCommands(prefix){
|
|
|
|
|
const q=prefix.toLowerCase();
|
2026-04-19 05:37:44 +00:00
|
|
|
const matches=COMMANDS.filter(c=>c.name.startsWith(q)).map(c=>({...c,source:'builtin'}));
|
|
|
|
|
const seen=new Set(matches.map(c=>c.name));
|
2026-04-20 22:04:28 +00:00
|
|
|
for(const [name, spec] of Object.entries(SLASH_SUBARG_SOURCES)){
|
|
|
|
|
if(!name.startsWith(q)||seen.has(name))continue;
|
|
|
|
|
matches.push({
|
|
|
|
|
name,
|
|
|
|
|
desc:spec.desc,
|
|
|
|
|
arg:'name',
|
|
|
|
|
source:'subarg-command',
|
|
|
|
|
});
|
|
|
|
|
seen.add(name);
|
|
|
|
|
}
|
2026-04-19 05:37:44 +00:00
|
|
|
for(const skill of _skillCommandCache){
|
|
|
|
|
if(!skill.name.startsWith(q)||seen.has(skill.name))continue;
|
|
|
|
|
matches.push(skill);
|
2026-04-20 22:04:28 +00:00
|
|
|
seen.add(skill.name);
|
2026-04-19 05:37:44 +00:00
|
|
|
}
|
|
|
|
|
return matches;
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 22:04:28 +00:00
|
|
|
let _slashModelCache=null;
|
|
|
|
|
let _slashModelCachePromise=null;
|
|
|
|
|
let _slashPersonalityCache=null;
|
|
|
|
|
let _slashPersonalityCachePromise=null;
|
|
|
|
|
|
|
|
|
|
function _normalizeSlashSubArg(value){
|
|
|
|
|
return String(value||'').trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _getSlashModelSubArgsFromDom(){
|
|
|
|
|
const sel=$('modelSelect');
|
|
|
|
|
if(!sel) return [];
|
|
|
|
|
const values=[];
|
|
|
|
|
for(const opt of Array.from(sel.options||[])){
|
|
|
|
|
const value=_normalizeSlashSubArg(opt.value||opt.textContent||'');
|
|
|
|
|
if(value) values.push(value);
|
|
|
|
|
}
|
|
|
|
|
return Array.from(new Set(values)).sort((a,b)=>a.localeCompare(b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function _loadSlashModelSubArgs(force=false){
|
|
|
|
|
const domValues=_getSlashModelSubArgsFromDom();
|
|
|
|
|
if(domValues.length&&!force){
|
|
|
|
|
_slashModelCache=domValues;
|
|
|
|
|
return domValues;
|
|
|
|
|
}
|
|
|
|
|
if(_slashModelCache&&!force) return _slashModelCache;
|
|
|
|
|
if(_slashModelCachePromise&&!force) return _slashModelCachePromise;
|
|
|
|
|
_slashModelCachePromise=(async()=>{
|
|
|
|
|
try{
|
|
|
|
|
const data=await api('/api/models');
|
|
|
|
|
const values=[];
|
|
|
|
|
for(const group of (data&&data.groups)||[]){
|
|
|
|
|
for(const model of (group&&group.models)||[]){
|
|
|
|
|
const id=_normalizeSlashSubArg(model&&model.id);
|
|
|
|
|
if(id) values.push(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const deduped=Array.from(new Set(values)).sort((a,b)=>a.localeCompare(b));
|
|
|
|
|
_slashModelCache=deduped;
|
|
|
|
|
return deduped;
|
|
|
|
|
}catch(_){
|
|
|
|
|
_slashModelCache=domValues;
|
|
|
|
|
return domValues;
|
|
|
|
|
}finally{
|
|
|
|
|
_slashModelCachePromise=null;
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
return _slashModelCachePromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function _loadSlashPersonalitySubArgs(force=false){
|
|
|
|
|
if(_slashPersonalityCache&&!force) return _slashPersonalityCache;
|
|
|
|
|
if(_slashPersonalityCachePromise&&!force) return _slashPersonalityCachePromise;
|
|
|
|
|
_slashPersonalityCachePromise=(async()=>{
|
|
|
|
|
try{
|
|
|
|
|
const data=await api('/api/personalities');
|
|
|
|
|
const values=['none'];
|
|
|
|
|
for(const p of (data&&data.personalities)||[]){
|
|
|
|
|
const name=_normalizeSlashSubArg(p&&p.name);
|
|
|
|
|
if(name) values.push(name);
|
|
|
|
|
}
|
|
|
|
|
const deduped=Array.from(new Set(values)).sort((a,b)=>a.localeCompare(b));
|
|
|
|
|
_slashPersonalityCache=deduped;
|
|
|
|
|
return deduped;
|
|
|
|
|
}catch(_){
|
|
|
|
|
_slashPersonalityCache=['none'];
|
|
|
|
|
return _slashPersonalityCache;
|
|
|
|
|
}finally{
|
|
|
|
|
_slashPersonalityCachePromise=null;
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
return _slashPersonalityCachePromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _getSlashSubArgOptions(spec){
|
|
|
|
|
if(Array.isArray(spec)) return Promise.resolve(spec.slice());
|
|
|
|
|
if(spec==='models') return _loadSlashModelSubArgs();
|
|
|
|
|
if(spec==='personalities') return _loadSlashPersonalitySubArgs();
|
|
|
|
|
return Promise.resolve([]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _parseSlashAutocomplete(text){
|
|
|
|
|
if(!text.startsWith('/')||text.indexOf('\n')!==-1) return null;
|
|
|
|
|
const raw=text.slice(1);
|
|
|
|
|
const hasSpace=/\s/.test(raw);
|
|
|
|
|
const parts=raw.split(/\s+/);
|
|
|
|
|
const cmdName=(parts[0]||'').toLowerCase();
|
|
|
|
|
const command=COMMANDS.find(c=>c.name===cmdName);
|
|
|
|
|
const subArgSource=(command&&command.subArgs)?command:SLASH_SUBARG_SOURCES[cmdName];
|
|
|
|
|
if(!hasSpace||!subArgSource){
|
|
|
|
|
return {kind:'commands', query:raw};
|
|
|
|
|
}
|
|
|
|
|
const argText=raw.slice(cmdName.length).replace(/^\s+/,'');
|
|
|
|
|
return {kind:'subargs', command:{name:cmdName, desc:subArgSource.desc, subArgs:subArgSource.subArgs}, query:argText.toLowerCase(), rawQuery:argText};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getSlashAutocompleteMatches(text){
|
|
|
|
|
const parsed=_parseSlashAutocomplete(text);
|
|
|
|
|
if(!parsed) return [];
|
|
|
|
|
if(parsed.kind==='commands') return getMatchingCommands(parsed.query);
|
|
|
|
|
const options=await _getSlashSubArgOptions(parsed.command.subArgs);
|
|
|
|
|
return options
|
|
|
|
|
.filter(opt=>String(opt).toLowerCase().startsWith(parsed.query))
|
|
|
|
|
.map(opt=>({
|
|
|
|
|
name:parsed.command.name,
|
|
|
|
|
value:String(opt),
|
|
|
|
|
desc:parsed.command.desc,
|
|
|
|
|
source:'subarg',
|
|
|
|
|
parent:parsed.command.name,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 06:55:04 +00:00
|
|
|
function _compressionAnchorMessageKey(m){
|
|
|
|
|
if(!m||!m.role||m.role==='tool') return null;
|
|
|
|
|
let content='';
|
|
|
|
|
try{
|
|
|
|
|
content=typeof msgContent==='function' ? String(msgContent(m)||'') : String(m.content||'');
|
|
|
|
|
}catch(_){
|
|
|
|
|
content=String(m.content||'');
|
|
|
|
|
}
|
|
|
|
|
const norm=content.replace(/\s+/g,' ').trim().slice(0,160);
|
|
|
|
|
const ts=m._ts||m.timestamp||null;
|
|
|
|
|
const attachments=Array.isArray(m.attachments)?m.attachments.length:0;
|
|
|
|
|
if(!norm && !attachments && !ts) return null;
|
|
|
|
|
return {role:String(m.role||''), ts, text:norm, attachments};
|
|
|
|
|
}
|
|
|
|
|
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
// ── Command handlers ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function cmdHelp(){
|
|
|
|
|
const lines=COMMANDS.map(c=>{
|
2026-04-18 06:55:04 +00:00
|
|
|
const usage=c.arg ? (String(c.arg).startsWith('[') ? ` ${c.arg}` : ` <${c.arg}>`) : '';
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
return ` /${c.name}${usage} — ${c.desc}`;
|
|
|
|
|
});
|
2026-04-08 14:55:03 +00:00
|
|
|
const msg={role:'assistant',content:t('available_commands')+'\n'+lines.join('\n')};
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
S.messages.push(msg);
|
|
|
|
|
renderMessages();
|
2026-04-08 14:55:03 +00:00
|
|
|
showToast(t('type_slash'));
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cmdClear(){
|
|
|
|
|
if(!S.session)return;
|
|
|
|
|
S.messages=[];S.toolCalls=[];
|
|
|
|
|
clearLiveToolCards();
|
2026-04-18 06:55:04 +00:00
|
|
|
if(typeof clearCompressionUi==='function') clearCompressionUi();
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
renderMessages();
|
|
|
|
|
$('emptyState').style.display='';
|
2026-04-08 14:55:03 +00:00
|
|
|
showToast(t('conversation_cleared'));
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function cmdModel(args){
|
2026-04-08 14:55:03 +00:00
|
|
|
if(!args){showToast(t('model_usage'));return;}
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
const sel=$('modelSelect');
|
|
|
|
|
if(!sel)return;
|
|
|
|
|
const q=args.toLowerCase();
|
|
|
|
|
// Fuzzy match: find first option whose label or value contains the query
|
|
|
|
|
let match=null;
|
|
|
|
|
for(const opt of sel.options){
|
|
|
|
|
if(opt.value.toLowerCase().includes(q)||opt.textContent.toLowerCase().includes(q)){
|
|
|
|
|
match=opt.value;break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-08 14:55:03 +00:00
|
|
|
if(!match){showToast(t('no_model_match')+`"${args}"`);return;}
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
sel.value=match;
|
|
|
|
|
await sel.onchange();
|
2026-04-08 14:55:03 +00:00
|
|
|
showToast(t('switched_to')+match);
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function cmdWorkspace(args){
|
2026-04-08 14:55:03 +00:00
|
|
|
if(!args){showToast(t('workspace_usage'));return;}
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
try{
|
|
|
|
|
const data=await api('/api/workspaces');
|
|
|
|
|
const q=args.toLowerCase();
|
|
|
|
|
const ws=(data.workspaces||[]).find(w=>
|
|
|
|
|
(w.name||'').toLowerCase().includes(q)||w.path.toLowerCase().includes(q)
|
|
|
|
|
);
|
2026-04-08 14:55:03 +00:00
|
|
|
if(!ws){showToast(t('no_workspace_match')+`"${args}"`);return;}
|
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242)
* Polish workspace panel behavior and app dialogs
* Replace remaining emoji UI glyphs with Lucide icons
* Redesign composer footer around model and context controls
Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill.
Design credit and inspiration: Theo / T3 Code.
Reference implementation: https://github.com/pingdotgg/t3code/
* Remove obsolete activity bar
Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts.
This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status.
* Move workspace and model switching into composer footer
* Move profile switching into composer footer
* Refactor Hermes control center UI
* Redesign control center settings modal layout
Widen the modal to 860px, simplify the tab list to icon+label rows,
stretch the tab column's divider to full height, lock the panel to a
fixed height so switching tabs no longer resizes the outer shell, and
always open on the Conversation tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Put session item actions in a dropdown
* Use Hermes mark in sidebar control button
* Reset control center section on close
* Drop session-item left border indicator
Remove the left-border accent used for active, CLI, and project rows —
each state already has a dedicated cue (gold fill, cli badge, project
dot), so the border was redundant. Fully round the row, add 2px
bottom spacing between rows, and strip the matching JS/CSS overrides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Increase session search input vertical padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Normalise odd pixel values across UI
Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid
across composer chips, sidebar panels, cron list, settings, approval
buttons, dropdowns, and inline message edit — eliminating the 7/9/11px
drift that was making sibling elements feel subtly misaligned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite)
The mobile layout regression suite (test_mobile_layout.py) requires:
- #btnMobileFiles onclick=toggleMobileFiles() in topbar chips
- .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints
Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports.
* Improve composer footer mobile responsiveness and UX
- Collapse composer chips to icon-only at <=400px viewports
- Add model chip icon (CPU) so it remains tappable when labels are hidden
- Show send button always (disabled state when empty, hidden during streaming)
- Show context usage indicator on session load, not just after streaming
- Add cancel status fallback timeout to prevent stale "Cancelling..." text
- Update tests to match new send button and busy state behavior
* Fix duplicate files button and broken workspace close on mobile
Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle
in the mobile topbar. Fix workspace panel close button calling undefined
closeMobileFiles() — now calls closeWorkspacePanel().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model chip icon vertical alignment in composer footer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix workspace toggle button hidden on desktop by conflicting CSS class
Remove mobile-files-btn class from #btnWorkspacePanelToggle — its
display:none!important rule was overriding workspace-toggle-btn visibility
on non-mobile viewports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix session actions dots button inaccessible on mobile sidebar
Always show the session actions trigger on mobile (no hover state on
touch devices) and restore right padding so text truncates with
ellipsis before the dots icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer footer manage links not opening sidebar panel
The "Manage profiles" and "Manage workspaces" links in the composer
footer dropdowns called switchPanel() which only changes the active
panel content but doesn't open the sidebar. Replaced with
mobileSwitchPanel() which also opens the sidebar so the panel is
actually visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Widen icon-only composer chips breakpoint from 400px to 768px
Move the icon-only chip styling up into the existing max-width:768px
media query so chips collapse to icon-only on tablets too, preventing
composer footer overflow on mid-size screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix composer-left vertical scrollbar by setting overflow-y:hidden
When overflow-x is set to auto, the CSS spec implicitly changes
overflow-y from visible to auto, allowing a vertical scrollbar to
appear from slight chip padding/border overflow. Explicitly set
overflow-y:hidden to prevent this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve rebase conflicts and fix control center test assertions
- Resolved 4 conflicts during rebase onto master (workspace.js,
boot.js, index.html, test_sprint34.py)
- Fixed test_sprint34.py: _controlSection -> _settingsSection,
cc-tab -> settings-tabs (matching actual implementation)
- Fixed quoting syntax error in test assertion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update version badge in System tab to v0.49.4
* docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge
---------
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
|
|
|
if(typeof switchToWorkspace==='function') await switchToWorkspace(ws.path, ws.name||ws.path);
|
|
|
|
|
else showToast(t('switched_workspace')+(ws.name||ws.path));
|
2026-04-08 14:55:03 +00:00
|
|
|
}catch(e){showToast(t('workspace_switch_failed')+e.message);}
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function cmdNew(){
|
2026-04-18 06:55:04 +00:00
|
|
|
if(typeof clearCompressionUi==='function') clearCompressionUi();
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
await newSession();
|
|
|
|
|
await renderSessionList();
|
|
|
|
|
$('msg').focus();
|
2026-04-08 14:55:03 +00:00
|
|
|
showToast(t('new_session'));
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-18 06:55:04 +00:00
|
|
|
async function _runManualCompression(focusTopic){
|
|
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
|
|
|
|
let visibleCount=0;
|
|
|
|
|
try{
|
|
|
|
|
const sid=S.session.session_id;
|
|
|
|
|
// Preflight: verify the viewed session still exists before compressing.
|
|
|
|
|
// This avoids a confusing "not found" toast when the UI is stale.
|
|
|
|
|
try{
|
|
|
|
|
const live=await api(`/api/session?session_id=${encodeURIComponent(sid)}`);
|
|
|
|
|
if(!live||!live.session||live.session.session_id!==sid){
|
|
|
|
|
throw new Error('session no longer available');
|
|
|
|
|
}
|
|
|
|
|
S.session=live.session;
|
|
|
|
|
S.messages=live.session.messages||[];
|
|
|
|
|
S.toolCalls=live.session.tool_calls||[];
|
|
|
|
|
}catch(preflightErr){
|
|
|
|
|
if(typeof clearCompressionUi==='function') clearCompressionUi();
|
|
|
|
|
if(typeof _setCompressionSessionLock==='function') _setCompressionSessionLock(null);
|
|
|
|
|
if(typeof setBusy==='function') setBusy(false);
|
|
|
|
|
if(typeof setComposerStatus==='function') setComposerStatus('');
|
|
|
|
|
renderMessages();
|
|
|
|
|
showToast('Compression failed: '+(preflightErr.message||'session no longer available'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if(typeof setBusy==='function') setBusy(true);
|
|
|
|
|
const body={session_id:sid};
|
|
|
|
|
if(focusTopic) body.focus_topic=focusTopic;
|
|
|
|
|
const visibleMessages=(S.messages||[]).filter(m=>{
|
|
|
|
|
if(!m||!m.role||m.role==='tool') return false;
|
|
|
|
|
if(m.role==='assistant'){
|
|
|
|
|
const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
|
|
|
|
|
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
|
|
|
|
|
if(hasTc||hasTu|| (typeof _messageHasReasoningPayload==='function' && _messageHasReasoningPayload(m))) return true;
|
|
|
|
|
}
|
|
|
|
|
return typeof msgContent==='function' ? !!msgContent(m) || !!m.attachments?.length : !!m.content || !!m.attachments?.length;
|
|
|
|
|
});
|
|
|
|
|
visibleCount=visibleMessages.length;
|
|
|
|
|
const anchorVisibleIdx=Math.max(0, visibleCount - 1);
|
|
|
|
|
const anchorMessageKey=_compressionAnchorMessageKey(visibleMessages[visibleMessages.length-1]||null);
|
|
|
|
|
const commandText=focusTopic?`/compress ${focusTopic}`:'/compress';
|
|
|
|
|
if(typeof setCompressionUi==='function'){
|
|
|
|
|
setCompressionUi({
|
|
|
|
|
sessionId:S.session.session_id,
|
|
|
|
|
phase:'running',
|
|
|
|
|
focusTopic:focusTopic||'',
|
|
|
|
|
commandText,
|
|
|
|
|
beforeCount:visibleCount,
|
|
|
|
|
anchorVisibleIdx,
|
|
|
|
|
anchorMessageKey,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if(typeof setComposerStatus==='function') setComposerStatus(t('compressing'));
|
|
|
|
|
renderMessages();
|
|
|
|
|
const data=await api('/api/session/compress',{method:'POST',body:JSON.stringify(body)});
|
|
|
|
|
if(data&&data.session){
|
|
|
|
|
const currentSid=S.session&&S.session.session_id;
|
|
|
|
|
if(data.session.session_id&&data.session.session_id!==currentSid){
|
|
|
|
|
await loadSession(data.session.session_id);
|
|
|
|
|
}else{
|
|
|
|
|
S.session=data.session;
|
|
|
|
|
S.messages=data.session.messages||[];
|
|
|
|
|
S.toolCalls=data.session.tool_calls||[];
|
|
|
|
|
clearLiveToolCards();
|
|
|
|
|
localStorage.setItem('hermes-webui-session',S.session.session_id);
|
|
|
|
|
syncTopbar();
|
|
|
|
|
renderMessages();
|
|
|
|
|
await renderSessionList();
|
|
|
|
|
updateQueueBadge(S.session.session_id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const summary=data&&data.summary;
|
|
|
|
|
if(typeof setCompressionUi==='function'&&S.session){
|
|
|
|
|
const referenceMsg=(S.messages||[]).find(m=>typeof _isContextCompactionMessage==='function'&&_isContextCompactionMessage(m));
|
2026-04-19 04:29:07 +00:00
|
|
|
const messageRef=referenceMsg?msgContent(referenceMsg)||String(referenceMsg.content||''):'';
|
2026-04-18 06:55:04 +00:00
|
|
|
const summaryRef=summary&&typeof summary.reference_message==='string' ? String(summary.reference_message||'').trim() : '';
|
2026-04-19 04:29:07 +00:00
|
|
|
// Prefer the persisted compaction handoff when it already exists in session state.
|
|
|
|
|
// The short summary fallback is only for environments where that message is unavailable.
|
|
|
|
|
const referenceText=messageRef || summaryRef;
|
2026-04-18 06:55:04 +00:00
|
|
|
const effectiveFocus=(data&&data.focus_topic)||focusTopic||'';
|
|
|
|
|
setCompressionUi({
|
|
|
|
|
sessionId:S.session.session_id,
|
|
|
|
|
phase:'done',
|
|
|
|
|
focusTopic:effectiveFocus,
|
|
|
|
|
commandText:effectiveFocus?`/compress ${effectiveFocus}`:'/compress',
|
|
|
|
|
beforeCount:visibleCount,
|
|
|
|
|
summary:summary||null,
|
|
|
|
|
referenceText,
|
|
|
|
|
anchorVisibleIdx: data?.session?.compression_anchor_visible_idx,
|
|
|
|
|
anchorMessageKey: data?.session?.compression_anchor_message_key||null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if(typeof setComposerStatus==='function') setComposerStatus('');
|
|
|
|
|
renderMessages();
|
|
|
|
|
if(typeof _setCompressionSessionLock==='function') _setCompressionSessionLock(null);
|
|
|
|
|
}catch(e){
|
|
|
|
|
if(typeof setCompressionUi==='function'){
|
|
|
|
|
const currentSid=S.session&&S.session.session_id;
|
|
|
|
|
setCompressionUi({
|
|
|
|
|
sessionId:currentSid||'',
|
|
|
|
|
phase:'error',
|
|
|
|
|
focusTopic:(focusTopic||'').trim(),
|
|
|
|
|
commandText:focusTopic?`/compress ${focusTopic}`:'/compress',
|
|
|
|
|
beforeCount:(S.messages||[]).filter(m=>m&&m.role&&m.role!=='tool').length,
|
|
|
|
|
errorText:`Compression failed: ${e.message}`,
|
|
|
|
|
anchorVisibleIdx: Math.max(0, visibleCount - 1),
|
|
|
|
|
anchorMessageKey:null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if(typeof _setCompressionSessionLock==='function') _setCompressionSessionLock(null);
|
|
|
|
|
if(typeof setBusy==='function') setBusy(false);
|
|
|
|
|
if(typeof setComposerStatus==='function') setComposerStatus('');
|
|
|
|
|
renderMessages();
|
|
|
|
|
showToast('Compression failed: '+e.message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if(typeof setBusy==='function') setBusy(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function cmdCompress(args){
|
|
|
|
|
await _runManualCompression((args||'').trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function cmdCompact(args){
|
|
|
|
|
await _runManualCompression((args||'').trim());
|
2026-04-05 01:46:34 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 02:04:41 +00:00
|
|
|
async function cmdUsage(){
|
|
|
|
|
const next=!window._showTokenUsage;
|
|
|
|
|
window._showTokenUsage=next;
|
|
|
|
|
try{
|
|
|
|
|
await api('/api/settings',{method:'POST',body:JSON.stringify({show_token_usage:next})});
|
|
|
|
|
}catch(e){}
|
|
|
|
|
// Update the settings checkbox if the panel is open
|
|
|
|
|
const cb=$('settingsShowTokenUsage');
|
|
|
|
|
if(cb) cb.checked=next;
|
|
|
|
|
renderMessages();
|
2026-04-08 14:55:03 +00:00
|
|
|
showToast(next?t('token_usage_on'):t('token_usage_off'));
|
2026-04-04 02:04:41 +00:00
|
|
|
}
|
|
|
|
|
|
feat: Sprint 26 — pluggable UI themes (dark, light, solarized, monokai, nord)
Five built-in themes with instant switching, persistent preference,
and zero-flicker loading. Custom themes are pure CSS additions.
Theme system:
- CSS variable overrides via :root[data-theme="name"] blocks
- Flicker prevention: inline <script> reads localStorage before
stylesheet parses, preventing dark-flash on light-mode users
- Server-side persistence via settings.json (theme field)
- Boot.js syncs server preference to DOM + localStorage
Built-in themes:
- Dark (default): deep navy/indigo, muted blue accents
- Light: clean white/gray, high contrast, scrollbar overrides
- Solarized Dark: teal background, warm accents
- Monokai: warm dark, green/pink accents
- Nord: arctic blue-gray, calm and minimal
UI integration:
- Settings panel: theme dropdown with instant live preview
- /theme slash command: /theme dark|light|solarized|monokai|nord
- No enum constraint on theme setting — custom themes just work
Documentation:
- THEMES.md: how to switch themes, create custom themes, contribute
8 new tests. All 408 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 03:48:05 +00:00
|
|
|
async function cmdTheme(args){
|
2026-04-18 06:37:09 +00:00
|
|
|
const themes=['system','dark','light'];
|
|
|
|
|
const skins=(_SKINS||[]).map(s=>s.name.toLowerCase());
|
|
|
|
|
const legacyThemes=Object.keys(_LEGACY_THEME_MAP||{});
|
|
|
|
|
const val=(args||'').toLowerCase().trim();
|
|
|
|
|
// Check if it's a theme
|
|
|
|
|
if(themes.includes(val)||legacyThemes.includes(val)){
|
|
|
|
|
const appearance=_normalizeAppearance(
|
|
|
|
|
val,
|
|
|
|
|
legacyThemes.includes(val)?null:localStorage.getItem('hermes-skin')
|
|
|
|
|
);
|
|
|
|
|
localStorage.setItem('hermes-theme',appearance.theme);
|
|
|
|
|
localStorage.setItem('hermes-skin',appearance.skin);
|
|
|
|
|
_applyTheme(appearance.theme);
|
|
|
|
|
_applySkin(appearance.skin);
|
|
|
|
|
try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:appearance.theme,skin:appearance.skin})});}catch(e){}
|
|
|
|
|
const sel=$('settingsTheme');
|
|
|
|
|
if(sel)sel.value=appearance.theme;
|
|
|
|
|
const skinSel=$('settingsSkin');
|
|
|
|
|
if(skinSel)skinSel.value=appearance.skin;
|
|
|
|
|
if(typeof _syncThemePicker==='function') _syncThemePicker(appearance.theme);
|
|
|
|
|
if(typeof _syncSkinPicker==='function') _syncSkinPicker(appearance.skin);
|
|
|
|
|
showToast(t('theme_set')+appearance.theme+(legacyThemes.includes(val)?` + ${appearance.skin}`:''));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Check if it's a skin
|
|
|
|
|
if(skins.includes(val)){
|
|
|
|
|
const appearance=_normalizeAppearance(localStorage.getItem('hermes-theme'),val);
|
|
|
|
|
localStorage.setItem('hermes-theme',appearance.theme);
|
|
|
|
|
localStorage.setItem('hermes-skin',appearance.skin);
|
|
|
|
|
_applyTheme(appearance.theme);
|
|
|
|
|
_applySkin(appearance.skin);
|
|
|
|
|
try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:appearance.theme,skin:appearance.skin})});}catch(e){}
|
|
|
|
|
const sel=$('settingsSkin');
|
|
|
|
|
if(sel)sel.value=appearance.skin;
|
|
|
|
|
const themeSel=$('settingsTheme');
|
|
|
|
|
if(themeSel)themeSel.value=appearance.theme;
|
|
|
|
|
if(typeof _syncThemePicker==='function') _syncThemePicker(appearance.theme);
|
|
|
|
|
if(typeof _syncSkinPicker==='function') _syncSkinPicker(appearance.skin);
|
|
|
|
|
showToast(t('theme_set')+appearance.skin);
|
feat: Sprint 26 — pluggable UI themes (dark, light, solarized, monokai, nord)
Five built-in themes with instant switching, persistent preference,
and zero-flicker loading. Custom themes are pure CSS additions.
Theme system:
- CSS variable overrides via :root[data-theme="name"] blocks
- Flicker prevention: inline <script> reads localStorage before
stylesheet parses, preventing dark-flash on light-mode users
- Server-side persistence via settings.json (theme field)
- Boot.js syncs server preference to DOM + localStorage
Built-in themes:
- Dark (default): deep navy/indigo, muted blue accents
- Light: clean white/gray, high contrast, scrollbar overrides
- Solarized Dark: teal background, warm accents
- Monokai: warm dark, green/pink accents
- Nord: arctic blue-gray, calm and minimal
UI integration:
- Settings panel: theme dropdown with instant live preview
- /theme slash command: /theme dark|light|solarized|monokai|nord
- No enum constraint on theme setting — custom themes just work
Documentation:
- THEMES.md: how to switch themes, create custom themes, contribute
8 new tests. All 408 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 03:48:05 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-04-18 06:37:09 +00:00
|
|
|
showToast(t('theme_usage')+themes.join('|')+' | '+skins.join('|')+' | legacy:'+legacyThemes.join('|'));
|
feat: Sprint 26 — pluggable UI themes (dark, light, solarized, monokai, nord)
Five built-in themes with instant switching, persistent preference,
and zero-flicker loading. Custom themes are pure CSS additions.
Theme system:
- CSS variable overrides via :root[data-theme="name"] blocks
- Flicker prevention: inline <script> reads localStorage before
stylesheet parses, preventing dark-flash on light-mode users
- Server-side persistence via settings.json (theme field)
- Boot.js syncs server preference to DOM + localStorage
Built-in themes:
- Dark (default): deep navy/indigo, muted blue accents
- Light: clean white/gray, high contrast, scrollbar overrides
- Solarized Dark: teal background, warm accents
- Monokai: warm dark, green/pink accents
- Nord: arctic blue-gray, calm and minimal
UI integration:
- Settings panel: theme dropdown with instant live preview
- /theme slash command: /theme dark|light|solarized|monokai|nord
- No enum constraint on theme setting — custom themes just work
Documentation:
- THEMES.md: how to switch themes, create custom themes, contribute
8 new tests. All 408 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 03:48:05 +00:00
|
|
|
}
|
|
|
|
|
|
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
|
|
|
async function cmdSkills(args){
|
|
|
|
|
try{
|
|
|
|
|
const data = await api('/api/skills');
|
|
|
|
|
let skills = data.skills || [];
|
|
|
|
|
if(args){
|
|
|
|
|
const q = args.toLowerCase();
|
|
|
|
|
skills = skills.filter(s =>
|
|
|
|
|
(s.name||'').toLowerCase().includes(q) ||
|
|
|
|
|
(s.description||'').toLowerCase().includes(q) ||
|
|
|
|
|
(s.category||'').toLowerCase().includes(q)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if(!skills.length){
|
|
|
|
|
const msg = {role:'assistant', content: args ? `No skills matching "${args}".` : 'No skills found.'};
|
|
|
|
|
S.messages.push(msg); renderMessages(); return;
|
|
|
|
|
}
|
|
|
|
|
// Group by category
|
|
|
|
|
const byCategory = {};
|
|
|
|
|
skills.forEach(s => {
|
|
|
|
|
const cat = s.category || 'General';
|
|
|
|
|
if(!byCategory[cat]) byCategory[cat] = [];
|
|
|
|
|
byCategory[cat].push(s);
|
|
|
|
|
});
|
|
|
|
|
const lines = [];
|
|
|
|
|
for(const [cat, items] of Object.entries(byCategory).sort()){
|
|
|
|
|
lines.push(`**${cat}**`);
|
|
|
|
|
items.forEach(s => {
|
|
|
|
|
const desc = s.description ? ` — ${s.description.slice(0,80)}${s.description.length>80?'...':''}` : '';
|
|
|
|
|
lines.push(` \`${s.name}\`${desc}`);
|
|
|
|
|
});
|
|
|
|
|
lines.push('');
|
|
|
|
|
}
|
|
|
|
|
const header = args
|
|
|
|
|
? `Skills matching "${args}" (${skills.length}):\n\n`
|
|
|
|
|
: `Available skills (${skills.length}):\n\n`;
|
|
|
|
|
S.messages.push({role:'assistant', content: header + lines.join('\n')});
|
|
|
|
|
renderMessages();
|
|
|
|
|
showToast(t('type_slash'));
|
|
|
|
|
}catch(e){
|
|
|
|
|
showToast('Failed to load skills: '+e.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
async function cmdPersonality(args){
|
2026-04-08 14:55:03 +00:00
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
if(!args){
|
|
|
|
|
// List available personalities
|
|
|
|
|
try{
|
|
|
|
|
const data=await api('/api/personalities');
|
|
|
|
|
if(!data.personalities||!data.personalities.length){
|
2026-04-08 14:55:03 +00:00
|
|
|
showToast(t('no_personalities'));
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const list=data.personalities.map(p=>` **${p.name}**${p.description?' — '+p.description:''}`).join('\n');
|
2026-04-08 14:55:03 +00:00
|
|
|
S.messages.push({role:'assistant',content:t('available_personalities')+'\n\n'+list+t('personality_switch_hint')});
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
renderMessages();
|
2026-04-08 14:55:03 +00:00
|
|
|
}catch(e){showToast(t('personalities_load_failed'));}
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const name=args.trim();
|
|
|
|
|
if(name.toLowerCase()==='none'||name.toLowerCase()==='default'||name.toLowerCase()==='clear'){
|
|
|
|
|
try{
|
|
|
|
|
await api('/api/personality/set',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,name:''})});
|
2026-04-08 14:55:03 +00:00
|
|
|
showToast(t('personality_cleared'));
|
|
|
|
|
}catch(e){showToast(t('failed_colon')+e.message);}
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try{
|
|
|
|
|
const res=await api('/api/personality/set',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,name})});
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
S.messages.push({role:'assistant',content:t('personality_set')+`**${name}**`});
|
|
|
|
|
renderMessages();
|
2026-04-08 14:55:03 +00:00
|
|
|
showToast(t('personality_set')+name);
|
|
|
|
|
}catch(e){showToast(t('failed_colon')+e.message);}
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 05:37:44 +00:00
|
|
|
async function cmdStop(){
|
|
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
|
|
|
|
if(!S.activeStreamId){showToast(t('no_active_task'));return;}
|
|
|
|
|
if(typeof cancelStream==='function'){await cancelStream();showToast(t('stream_stopped'));}
|
|
|
|
|
else showToast(t('cancel_unavailable'));
|
|
|
|
|
}
|
|
|
|
|
async function cmdTitle(args){
|
|
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
|
|
|
|
const name=(args||'').trim();
|
|
|
|
|
if(!name){
|
|
|
|
|
S.messages.push({role:'assistant',content:`${t('title_current')}: **${S.session.title||t('untitled')}**\n\n${t('title_change_hint')}`});
|
|
|
|
|
renderMessages();return;
|
|
|
|
|
}
|
|
|
|
|
try{
|
|
|
|
|
const r=await api('/api/session/rename',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,title:name})});
|
|
|
|
|
if(r&&r.error){showToast(r.error);return;}
|
|
|
|
|
S.session.title=(r&&r.session&&r.session.title)||name;
|
|
|
|
|
if(typeof syncTopbar==='function')syncTopbar();
|
|
|
|
|
if(typeof renderSessionList==='function')renderSessionList();
|
|
|
|
|
showToast(`${t('title_set')} "${S.session.title}"`);
|
fix(ui): slash command input now echoed as user message in chat (closes #840)
* fix(ui): echo slash command input as user message in chat (#840)
Slash commands like /skills, /help, /status previously showed only the
assistant response with no user message above it — the conversation
appeared to start from nowhere.
Fix: executeCommand() now returns {noEcho:bool} instead of true/false
(returns null when no command matched). send() in messages.js pushes a
user message bubble before returning when noEcho is false.
Commands with noEcho:true are action-only and don't get echoed:
/clear, /new, /stop, /retry, /undo, /voice, /model, /workspace,
/theme, /usage, /reasoning.
Commands without noEcho (get echoed):
/help, /skills, /status, /title, /compress, /compact, /personality.
16 new tests in test_issue840_slash_echo.py.
* fix(ui): push user message BEFORE running slash handler (ordering bug)
The PR as originally written pushed the user message AFTER the slash
command handler ran. That works correctly for async handlers (the
assistant response lands later, after the user push) but breaks for
sync handlers like cmdHelp which push their assistant response
synchronously:
S.messages = [assistant response, user "/help"] ← reverse order
The chat would render the help content ABOVE the user's own "/help"
input — not what the issue asked for.
Fix: look up the command inline, push the user message first (for
echo-worthy commands), then run the handler. If the handler opts out
(returns false — e.g. /reasoning <level>), pop the user message back
off so the normal send path can add it cleanly when forwarding to the
agent.
Renamed the flow so it's clear we're not calling executeCommand twice
(my first attempt did that by accident). executeCommand() stays as a
public API returning null or {noEcho:bool} — just isn't the only path
send() uses now.
Added 2 regression tests:
- test_send_pushes_user_message_before_running_handler: asserts
the user push appears before the handler invocation in source order.
- test_send_rolls_back_user_push_on_handler_optout: asserts the
S.messages.pop() for the opt-out case.
Also tightened the existing `test_send_checks_noecho_flag` and
`test_send_pushes_user_message_for_echo_commands` tests to look at
the new `_cmd.noEcho` pattern inline (vs the original
`cmdResult.noEcho`). Removed `test_send_uses_null_check_not_truthy`
(obsoleted — the control flow no longer stores the executeCommand
return in a variable).
Full suite: 1767 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): compress/compact noEcho + title/personality confirmation messages
Applied Opus mentor review fixes:
- compress and compact: add noEcho:true (S.messages reset internally causes
user bubble to flicker/disappear without noEcho)
- /title <name>: push assistant confirmation message after rename succeeds
- /personality <name>: push assistant confirmation message after set succeeds
- 4 new regression tests covering the above invariants
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:08:24 +00:00
|
|
|
S.messages.push({role:'assistant',content:`${t('title_set')} **${S.session.title}**`});
|
|
|
|
|
renderMessages();
|
2026-04-19 05:37:44 +00:00
|
|
|
}catch(e){showToast(t('failed_colon')+e.message);}
|
|
|
|
|
}
|
|
|
|
|
async function cmdRetry(){
|
|
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
|
|
|
|
if(S.session.is_cli_session){showToast(t('cmd_webui_only_session'));return;}
|
|
|
|
|
const activeSid=S.session.session_id;
|
|
|
|
|
try{
|
|
|
|
|
const r=await api('/api/session/retry',{method:'POST',body:JSON.stringify({session_id:activeSid})});
|
|
|
|
|
if(r&&r.error){showToast(r.error);return;}
|
|
|
|
|
if(!S.session||S.session.session_id!==activeSid)return;
|
|
|
|
|
const data=await api('/api/session?session_id='+encodeURIComponent(activeSid));
|
|
|
|
|
if(data&&data.session){S.messages=data.session.messages||[];S.toolCalls=[];if(typeof clearLiveToolCards==='function')clearLiveToolCards();renderMessages();}
|
|
|
|
|
$('msg').value=r.last_user_text||'';if(typeof autoResize==='function')autoResize();await send();
|
|
|
|
|
}catch(e){showToast(t('retry_failed')+e.message);}
|
|
|
|
|
}
|
|
|
|
|
async function cmdUndo(){
|
|
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
|
|
|
|
if(S.session.is_cli_session){showToast(t('cmd_webui_only_session'));return;}
|
|
|
|
|
const activeSid=S.session.session_id;
|
|
|
|
|
try{
|
|
|
|
|
const r=await api('/api/session/undo',{method:'POST',body:JSON.stringify({session_id:activeSid})});
|
|
|
|
|
if(r&&r.error){showToast(r.error);return;}
|
|
|
|
|
if(!S.session||S.session.session_id!==activeSid)return;
|
|
|
|
|
const data=await api('/api/session?session_id='+encodeURIComponent(activeSid));
|
|
|
|
|
if(data&&data.session){S.messages=data.session.messages||[];S.toolCalls=[];if(typeof clearLiveToolCards==='function')clearLiveToolCards();renderMessages();}
|
|
|
|
|
showToast(`↩ ${t('undid_n_messages')} ${r.removed_count} ${t('undid_messages_suffix')}`);
|
|
|
|
|
}catch(e){showToast(t('undo_failed')+e.message);}
|
|
|
|
|
}
|
2026-04-24 01:24:51 +00:00
|
|
|
async function undoLastExchange(){await cmdUndo();}
|
|
|
|
|
async function cmdBtw(args){
|
|
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
|
|
|
|
const question=(args||'').trim();
|
|
|
|
|
if(!question){showToast(t('cmd_btw_usage'));return;}
|
|
|
|
|
showToast(t('btw_asking'));
|
|
|
|
|
const activeSid=S.session.session_id;
|
|
|
|
|
try{
|
|
|
|
|
const r=await api('/api/btw',{method:'POST',body:JSON.stringify({session_id:activeSid,question})});
|
|
|
|
|
if(r&&r.error){showToast(r.error);return;}
|
|
|
|
|
// Connect to the ephemeral SSE stream
|
|
|
|
|
const streamId=r.stream_id;
|
|
|
|
|
const parentSid=r.parent_session_id;
|
|
|
|
|
if(typeof attachBtwStream==='function') attachBtwStream(parentSid,streamId,question);
|
|
|
|
|
}catch(e){showToast(t('btw_failed')+e.message);}
|
|
|
|
|
}
|
|
|
|
|
async function cmdBackground(args){
|
|
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
|
|
|
|
const prompt=(args||'').trim();
|
|
|
|
|
if(!prompt){showToast(t('cmd_background_usage'));return;}
|
|
|
|
|
showToast(t('bg_running'));
|
|
|
|
|
const activeSid=S.session.session_id;
|
|
|
|
|
try{
|
|
|
|
|
const r=await api('/api/background',{method:'POST',body:JSON.stringify({session_id:activeSid,prompt})});
|
|
|
|
|
if(r&&r.error){showToast(r.error);return;}
|
|
|
|
|
// Show background badge and start polling
|
|
|
|
|
if(typeof showBackgroundBadge==='function') showBackgroundBadge(r.task_id);
|
|
|
|
|
if(typeof startBackgroundPolling==='function') startBackgroundPolling(activeSid,r.task_id,prompt);
|
|
|
|
|
}catch(e){showToast(t('bg_failed')+e.message);}
|
|
|
|
|
}
|
2026-04-19 05:37:44 +00:00
|
|
|
async function cmdStatus(){
|
|
|
|
|
if(!S.session){showToast(t('no_active_session'));return;}
|
|
|
|
|
try{
|
|
|
|
|
const r=await api('/api/session/status?session_id='+encodeURIComponent(S.session.session_id));
|
|
|
|
|
if(r&&r.error){showToast(r.error);return;}
|
|
|
|
|
S.messages.push({role:'assistant',content:[`**${t('status_heading')}**`,'',`**${t('status_session_id')}:** \`${r.session_id}\``,`**${t('status_title')}:** ${r.title||t('untitled')}`,`**${t('status_model')}:** ${r.model||t('usage_default_model')}`,`**${t('status_workspace')}:** ${r.workspace}`,`**${t('status_personality')}:** ${r.personality||t('usage_personality_none')}`,`**${t('status_messages')}:** ${r.message_count}`,`**${t('status_agent_running')}:** ${r.agent_running?t('status_yes'):t('status_no')}`,].join('\n')});
|
|
|
|
|
renderMessages();
|
|
|
|
|
}catch(e){showToast(t('status_load_failed')+e.message);}
|
|
|
|
|
}
|
2026-04-21 22:26:52 +00:00
|
|
|
function cmdReasoning(args){
|
|
|
|
|
const arg=(args||'').trim().toLowerCase();
|
|
|
|
|
const BRAIN='\uD83E\uDDE0';
|
|
|
|
|
// Matches hermes_constants.VALID_REASONING_EFFORTS + 'none' (CLI parity).
|
|
|
|
|
const EFFORTS=['none','minimal','low','medium','high','xhigh'];
|
|
|
|
|
// Shared status renderer used by the no-args branch and as a fallback.
|
|
|
|
|
function _fmtStatus(st){
|
|
|
|
|
const vis=(st && st.show_reasoning===false)?'off':'on';
|
|
|
|
|
const eff=(st && st.reasoning_effort)||'default';
|
|
|
|
|
return BRAIN+' Reasoning effort: '+eff+' \u00B7 display: '+vis
|
|
|
|
|
+' | /reasoning show|hide|none|minimal|low|medium|high|xhigh';
|
|
|
|
|
}
|
|
|
|
|
if(!arg){
|
|
|
|
|
// Status — read from the same config.yaml keys the CLI uses.
|
|
|
|
|
api('/api/reasoning').then(function(st){showToast(_fmtStatus(st));})
|
|
|
|
|
.catch(function(){showToast(BRAIN+' /reasoning — status unavailable');});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if(arg==='show'||arg==='on'||arg==='hide'||arg==='off'){
|
|
|
|
|
const on=(arg==='show'||arg==='on');
|
|
|
|
|
// Update the UI render gate immediately for responsiveness.
|
|
|
|
|
window._showThinking=on;
|
|
|
|
|
if(typeof renderMessages==='function') renderMessages();
|
|
|
|
|
// Persist via /api/reasoning → config.yaml display.show_reasoning
|
|
|
|
|
// (CLI reads the same key). Also mirror into WebUI settings.json
|
|
|
|
|
// show_thinking so boot.js picks it up on reload without hitting
|
|
|
|
|
// /api/reasoning on every page load.
|
|
|
|
|
api('/api/reasoning',{method:'POST',body:JSON.stringify({display:arg})}).catch(function(){});
|
|
|
|
|
api('/api/settings',{method:'POST',body:JSON.stringify({show_thinking:on})}).catch(function(){});
|
|
|
|
|
showToast(BRAIN+' Thinking blocks: '+(on?'on':'off')+' (saved)');
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if(EFFORTS.includes(arg)){
|
|
|
|
|
// Persist via /api/reasoning → config.yaml agent.reasoning_effort.
|
|
|
|
|
// Takes effect on the NEXT session/turn (agent re-reads config at
|
|
|
|
|
// construction time), matching CLI semantics where `/reasoning high`
|
|
|
|
|
// also forces an agent re-init.
|
|
|
|
|
api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:arg})})
|
|
|
|
|
.then(function(st){
|
|
|
|
|
const eff=(st && st.reasoning_effort)||arg;
|
2026-04-24 02:18:51 +00:00
|
|
|
showToast('Reasoning effort set to '+eff+' (saved; applies to next turn)');
|
|
|
|
|
if(typeof _applyReasoningChip==='function') _applyReasoningChip(eff);
|
2026-04-21 22:26:52 +00:00
|
|
|
})
|
|
|
|
|
.catch(function(e){
|
|
|
|
|
showToast(BRAIN+' Failed to set effort: '+(e && e.message ? e.message : arg));
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
showToast('Unknown argument: '+arg+' \u2014 use show|hide|'+EFFORTS.join('|'));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-04-19 05:37:44 +00:00
|
|
|
function cmdVoice(){
|
|
|
|
|
const mic=document.getElementById('btnMic');
|
|
|
|
|
if(mic&&mic.style.display!=='none'&&!mic.disabled){try{mic.click();return;}catch(_){}}
|
|
|
|
|
showToast(t('cmd_voice_use_mic'));
|
|
|
|
|
}
|
|
|
|
|
let _skillCommandCache=[];
|
|
|
|
|
let _skillCommandLoadPromise=null;
|
|
|
|
|
let _skillCommandCacheReady=false;
|
|
|
|
|
function _skillCommandSlug(name){
|
|
|
|
|
const raw=String(name||'').trim().toLowerCase();
|
|
|
|
|
if(!raw)return'';
|
|
|
|
|
return raw.replace(/[\s_]+/g,'-').replace(/[^a-z0-9-]/g,'').replace(/-{2,}/g,'-').replace(/^-+|-+$/g,'');
|
|
|
|
|
}
|
|
|
|
|
function _buildSkillCommandEntry(skill){
|
|
|
|
|
const skillName=String(skill&&skill.name||'').trim();
|
|
|
|
|
const slug=_skillCommandSlug(skillName);
|
|
|
|
|
if(!slug)return null;
|
|
|
|
|
if(COMMANDS.some(c=>c.name===slug)) return null;
|
|
|
|
|
return{name:slug,desc:String(skill&&skill.description||'').trim()||t('slash_skill_desc'),source:'skill',skillName};
|
|
|
|
|
}
|
|
|
|
|
async function loadSkillCommands(force=false){
|
|
|
|
|
if(_skillCommandCacheReady&&!force)return _skillCommandCache;
|
|
|
|
|
if(_skillCommandLoadPromise&&!force)return _skillCommandLoadPromise;
|
|
|
|
|
_skillCommandLoadPromise=(async()=>{
|
|
|
|
|
try{
|
|
|
|
|
const data=await api('/api/skills');
|
|
|
|
|
const deduped=new Map();
|
|
|
|
|
for(const skill of (data&&data.skills)||[]){const entry=_buildSkillCommandEntry(skill);if(entry&&!deduped.has(entry.name))deduped.set(entry.name,entry);}
|
|
|
|
|
_skillCommandCache=Array.from(deduped.values()).sort((a,b)=>a.name.localeCompare(b.name));
|
|
|
|
|
}catch(_){_skillCommandCache=[];}
|
|
|
|
|
finally{_skillCommandCacheReady=true;_skillCommandLoadPromise=null;}
|
|
|
|
|
return _skillCommandCache;
|
|
|
|
|
})();
|
|
|
|
|
return _skillCommandLoadPromise;
|
|
|
|
|
}
|
|
|
|
|
function refreshSlashCommandDropdown(){
|
|
|
|
|
const ta=$('msg');if(!ta)return;
|
|
|
|
|
const text=ta.value||'';
|
|
|
|
|
if(!text.startsWith('/')||text.indexOf('\n')!==-1){hideCmdDropdown();return;}
|
2026-04-20 22:04:28 +00:00
|
|
|
getSlashAutocompleteMatches(text).then(matches=>{
|
|
|
|
|
if(($('msg').value||'')!==text) return;
|
|
|
|
|
if(matches.length)showCmdDropdown(matches);else hideCmdDropdown();
|
|
|
|
|
});
|
2026-04-19 05:37:44 +00:00
|
|
|
}
|
|
|
|
|
function ensureSkillCommandsLoadedForAutocomplete(){
|
|
|
|
|
if(_skillCommandCacheReady||_skillCommandLoadPromise)return;
|
|
|
|
|
loadSkillCommands().then(()=>{refreshSlashCommandDropdown();});
|
|
|
|
|
}
|
|
|
|
|
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
// ── Autocomplete dropdown ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
let _cmdSelectedIdx=-1;
|
|
|
|
|
|
|
|
|
|
function showCmdDropdown(matches){
|
|
|
|
|
const dd=$('cmdDropdown');
|
|
|
|
|
if(!dd)return;
|
|
|
|
|
dd.innerHTML='';
|
2026-04-20 22:04:28 +00:00
|
|
|
_cmdSelectedIdx=matches.length?0:-1;
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
for(let i=0;i<matches.length;i++){
|
|
|
|
|
const c=matches[i];
|
|
|
|
|
const el=document.createElement('div');
|
|
|
|
|
el.className='cmd-item';
|
2026-04-20 22:04:28 +00:00
|
|
|
if(i===_cmdSelectedIdx) el.classList.add('selected');
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
el.dataset.idx=i;
|
2026-04-20 22:04:28 +00:00
|
|
|
const isSubArg=c.source==='subarg';
|
|
|
|
|
const usage=(!isSubArg&&c.arg)?` <span class="cmd-item-arg">${esc(c.arg)}</span>`:'';
|
2026-04-19 05:37:44 +00:00
|
|
|
const badge=c.source==='skill'?`<span class="cmd-item-badge cmd-item-badge-skill">${esc(t('slash_skill_badge'))}</span>`:'';
|
|
|
|
|
if(c.source==='skill') el.classList.add('cmd-item-skill');
|
2026-04-20 22:04:28 +00:00
|
|
|
const nameHtml=isSubArg
|
|
|
|
|
? `<div class="cmd-item-name"><span class="cmd-item-parent">/${esc(c.parent)}</span> <span class="cmd-item-subarg">${esc(c.value)}</span></div>`
|
|
|
|
|
: `<div class="cmd-item-name">/${esc(c.name)}${usage}${badge}</div>`;
|
|
|
|
|
const descHtml=`<div class="cmd-item-desc">${esc(c.desc)}</div>`;
|
|
|
|
|
el.innerHTML=`${nameHtml}${descHtml}`;
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
el.onmousedown=(e)=>{
|
|
|
|
|
e.preventDefault();
|
2026-04-20 22:04:28 +00:00
|
|
|
const nextValue=isSubArg?('/'+c.parent+' '+c.value):('/'+c.name+(c.arg?' ':''));
|
|
|
|
|
$('msg').value=nextValue;
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
$('msg').focus();
|
2026-04-20 22:04:28 +00:00
|
|
|
if(!isSubArg&&c.source!=='skill'&&nextValue.endsWith(' ')&&typeof getSlashAutocompleteMatches==='function'){
|
|
|
|
|
getSlashAutocompleteMatches(nextValue).then(matches=>{
|
|
|
|
|
if(($('msg').value||'')!==nextValue) return;
|
|
|
|
|
if(matches.length) showCmdDropdown(matches);
|
|
|
|
|
else hideCmdDropdown();
|
|
|
|
|
});
|
|
|
|
|
}else{
|
|
|
|
|
hideCmdDropdown();
|
|
|
|
|
}
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
};
|
|
|
|
|
dd.appendChild(el);
|
|
|
|
|
}
|
|
|
|
|
dd.classList.add('open');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideCmdDropdown(){
|
|
|
|
|
const dd=$('cmdDropdown');
|
|
|
|
|
if(dd)dd.classList.remove('open');
|
|
|
|
|
_cmdSelectedIdx=-1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function navigateCmdDropdown(dir){
|
|
|
|
|
const dd=$('cmdDropdown');
|
|
|
|
|
if(!dd)return;
|
|
|
|
|
const items=dd.querySelectorAll('.cmd-item');
|
|
|
|
|
if(!items.length)return;
|
|
|
|
|
items.forEach(el=>el.classList.remove('selected'));
|
|
|
|
|
_cmdSelectedIdx+=dir;
|
|
|
|
|
if(_cmdSelectedIdx<0)_cmdSelectedIdx=items.length-1;
|
|
|
|
|
if(_cmdSelectedIdx>=items.length)_cmdSelectedIdx=0;
|
|
|
|
|
items[_cmdSelectedIdx].classList.add('selected');
|
2026-04-22 05:55:09 +00:00
|
|
|
// Scroll the newly highlighted item into view so it stays visible when the
|
|
|
|
|
// dropdown overflows and the user navigates with keyboard (#838).
|
|
|
|
|
items[_cmdSelectedIdx].scrollIntoView({block:'nearest'});
|
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectCmdDropdownItem(){
|
|
|
|
|
const dd=$('cmdDropdown');
|
|
|
|
|
if(!dd)return;
|
|
|
|
|
const items=dd.querySelectorAll('.cmd-item');
|
|
|
|
|
if(_cmdSelectedIdx>=0&&_cmdSelectedIdx<items.length){
|
|
|
|
|
items[_cmdSelectedIdx].onmousedown({preventDefault:()=>{}});
|
|
|
|
|
} else if(items.length===1){
|
|
|
|
|
items[0].onmousedown({preventDefault:()=>{}});
|
|
|
|
|
}
|
|
|
|
|
hideCmdDropdown();
|
|
|
|
|
}
|
2026-04-19 05:37:44 +00:00
|
|
|
|
|
|
|
|
// ── Handler aliases (for test-discoverable command registration) ──────────────
|
|
|
|
|
// The COMMANDS array above is the authoritative dispatch table. These aliases
|
|
|
|
|
// allow tooling and tests to discover command handlers by name independently.
|
|
|
|
|
const HANDLERS = {};
|
|
|
|
|
HANDLERS.skills = cmdSkills;
|