- Added interrupt support for in-flight LLM responses in the Chat pane.
src/pktai_tui/app.py- When a prompt is sent, the blue "Send" button toggles to a red "Stop" button.
- Pressing "Stop" cancels the in-flight background worker running the LLM call.
- On cancel, the pending spinner row is removed and a system line "(generation stopped)" is appended.
- After completion or cancellation, the button reverts back to blue "Send".
- Internals: stores the worker handle (
self._current_worker) and catchesasyncio.CancelledErrorto clean up UI state.
- Type a prompt and press Send; to interrupt, press Stop.
- Streaming plus partial rendering would pair well with Stop for better UX; we can add token-level streaming next.
- TL;DR: Added a right-side, dynamically wrapping Chat pane powered by an Ollama-backed LLM (via OpenAI client), with Send/Enter-to-send and New Chat, while preserving the packets + details workflow.
- Name: pktai-tui
- Purpose: AI-assisted packet analysis in the terminal using Textual UI and PyShark.
- Entry point:
src/pktai_tui/app.py(PktaiTUI) - Dependencies: Textual, PyShark, textual-fspicker, OpenAI (for Ollama-compatible API)
PktaiTUI(src/pktai_tui/app.py)- Top-level chrome:
Header,Footer. - Main body: horizontal split (
#body).- Left (
#left):PacketList(#packets) shows parsed packets.Tree(#details) shows expandable per-layer details for the highlighted packet.
- Right (
#chat):ChatPanewith message log, input + send button, and New Chat button.
- Left (
- Top-level chrome:
- Parsing:
parse_capture()insrc/pktai_tui/services/capture.pyfeedsPacketListwithPacketRowentries. - Models:
PacketRowinsrc/pktai_tui/models.py(imported as.models). - File open:
textual-fspickerdialog triggered byokey.
- Added
ChatPane(insrc/pktai_tui/app.py):- UI:
RichLogfor dynamic soft-wrapping and auto-scroll, input box, Send button, New Chat button. - Layout: New 75/25 horizontal split; left contains Packets + Details; right is Chat.
- CSS: Ensures chat log fills available space (vertical scroll only), input row beneath, and a full-width slim green New Chat button.
- UI:
- Chat Functionality:
- Client:
AsyncOpenAIpointed to Ollama (OLLAMA_BASE_URL, defaulthttp://localhost:11434/v1). - Model:
qwen3:latestby default (override viaOLLAMA_MODEL). - History: Maintains per-session messages (user/assistant) and appends to log.
- Interactions: Click Send or press Enter to send; New Chat clears history and log.
- Error handling: UI notifications on failures.
- Client:
- Import/Widget Adjustments:
- Replaced
TextLogwithLog, then withRichLogfor true soft-wrapping. - Avoided attribute name clashes by using
chat_log,chat_input, etc.
- Replaced
- Dependencies:
pyproject.tomlupdated withopenai>=1.30.0.
- Ensure dependencies installed (e.g.,
uv sync). - Run Ollama locally and pull model once:
ollama run qwen3:latest. - Optional environment variables:
OLLAMA_BASE_URL(defaulthttp://localhost:11434/v1)OPENAI_API_KEY(defaultollama, required by the client but ignored by Ollama)OLLAMA_MODEL(defaultqwen3:latest)
- Start app: run the
pktaiscript (from[project.scripts]), orpython -m pktai_tui.app. - Open capture: press
oto open a.pcap/.pcapngfile.
- Introduced a right-side Chat pane with dynamic soft-wrapping, hooked to Ollama via OpenAI client, with Send/Enter submit and New Chat reset.
- Refactored layout to a 75/25 split: left for packets/details, right for chat.
- Updated dependencies and CSS to support the new UX.
-
Added speaker avatars in chat
- User:
👤; Assistant:🤖viaChatPane._make_avatar()and.avatarCSS class. - Adjusted avatar sizing and alignment (
width: 3, top margin1) to align with first text line.
- User:
-
Inline spinner during LLM processing
- Shows right after the user message.
- On response, the pending spinner row is removed and replaced with the final assistant message to avoid gaps.
.inline_spinner { width: auto; height: auto; }.
-
Thought process (reasoning) expander
- Parses
<think>...</think>and renders a collapsibleTreelabeled “Thought process” above assistant text. - Collapsed by default; constrained with
height: auto,min-height: 0,flex_grow: 0, and hidden guides for compactness. - Lines pre-wrapped (
textwrap.fill) and rendered withrich.text.Text(..., overflow="fold")to avoid overflow.
- Parses
-
Spacing and wrapping fixes
- Eliminated large vertical gaps by removing the extra pending row and constraining the reasoning tree.
- Tight but readable message spacing:
.msg { margin: 0 0 1 0; }. - Comfortable text padding:
.bubble { padding: 1; }and runtimebubble.styles.padding = 1. - Chat log gutter for breathing room:
#chat_log { padding: 1; }. - Ensured all containers use
height: auto; min-height: 0and no unintended flex growth.
-
Message rendering structure
- Each message row is
Horizontalwithavatar | bubbleand scrolls to end. - Assistant bubble: reasoning tree (if present) above main text content.
- Each message row is
src/pktai_tui/app.pyChatPane._append_message()ChatPane._send_and_get_reply()ChatPane._populate_assistant_bubble()- Embedded CSS for
#chat_log,.msg,.avatar,.bubble,.think_tree,.inline_spinner.
- Optional polish: rounded chat bubbles and subtle background color for messages.
- Potential keyboard shortcuts for expanding/collapsing the reasoning tree.
- Added Wireshark-like in-memory filtering module and integrated it into services and app flow.
src/pktai_tui/services/filtering.py- Wireshark-like display filter subset with tokenizer, parser, evaluator.
- Exports:
filter_packets(packets, display_filter),nl_to_display_filter(nl_query).
src/pktai_tui/services/capture.py- Extracted
build_packet_view(packet, index)for reuse when rebuilding UI from filtered packets. parse_capture(..., on_packet_obj=...)collects raw pyshark packets during parse.
- Extracted
src/pktai_tui/services/__init__.py- Re-exported
build_packet_view,filter_packets,nl_to_display_filter.
- Re-exported
src/pktai_tui/app.py- Stores raw packets (
self._raw_packets). - New methods:
rebuild_from_packets(packets)to repopulate UI from a given packet list.apply_display_filter(display_filter)to filter and refresh UI.apply_nl_query(nl_query)to convert NL → display filter and apply it.
- Chat input now supports slash-commands:
/df <display_filter>applies display filter without invoking the LLM.- Non-slash input continues to be sent to the LLM.
- Stores raw packets (
src/pktai_tui/filtering.py- Backwards-compat shim that re-exports from
pktai_tui.services.filteringwith a deprecation note.
- Backwards-compat shim that re-exports from
tests/test_filtering.py- Updated imports to
from pktai_tui.services.filtering ....
- Updated imports to
README.md- Updated examples to import from
pktai_tui.services.filteringand demonstratenl_to_display_filter.
- Updated examples to import from
- From TUI chat input:
/df ngap && sctp.dstport == 38412/df ip.src == 10.0.0.1 && tcp
- Programmatically within the app:
self.apply_display_filter("ngap && sctp.dstport == 38412")self.apply_nl_query("get me all ngap packets with dst port 38412")
- Consider caching parsed ASTs for repeat filters to speed up toggling.
- Provide a visible banner or status line showing the active display filter.
- Add more operators over time (e.g., contains, ranges) with clear error messages for unsupported ones.
- Introduced a services-layer abstraction for the LLM and added a compact, modal Settings screen with model selection and generation controls. Enhanced the chat pane to render Markdown natively.
src/pktai_tui/services/llm.py- Added
LLMServiceencapsulating OpenAI-compatible chat. from_env()readsOLLAMA_BASE_URL,OPENAI_API_KEY,OLLAMA_MODEL, and optionalLLM_TEMPERATURE.chat(messages, model, temperature, top_p, max_tokens, extra)to support per-call overrides.list_models()to enumerate models from the Ollama/OpenAI-compatible server.
- Added
src/pktai_tui/services/__init__.py- Exported
LLMService.
- Exported
src/pktai_tui/app.pyChatPanenow usesLLMServiceinstead of constructingAsyncOpenAIdirectly.- Added Settings shortcut binding:
("s", "open_settings", "Settings")and session-scoped overrides storage (_llm_overrides). - LLM calls apply overrides if present.
- Chat messages now render Markdown:
- Uses
textual.widgets.Markdownwhen available. - Fallback to
Staticwithrich.markdown.Markdownrenderable.
- Uses
src/pktai_tui/ui/settings.py- New
SettingsScreenas a centeredModalScreen(popover-style) with a slim dialog. - Model dropdown populated via
LLMService.list_models(); selects current or first available. - Controls: temperature and top_p (sliders when available, else inputs), max_tokens and context_window inputs.
- Save dismisses the modal and persists overrides to the app; Cancel/Esc discards.
- Robustness:
- Handles environments without
textual.widgets.Sliderby falling back to inputs. - Fixed duplicate ID issue by using
.rowclass for section containers (no repeated IDs).
- Handles environments without
- New
src/pktai_tui/ui/__init__.py- Exported
SettingsScreen.
- Exported
- Open Settings: press
s. - Pick a model and adjust temperature/top_p (sliders or inputs), set max tokens/context window, then Save.
- If Settings is never opened, env defaults are used (
OLLAMA_MODEL,LLM_TEMPERATURE, etc.). - Chat supports Markdown formatting in both user and assistant messages.
- Optional persistence: write overrides to a config file to survive restarts.
- Streaming responses could improve perceived latency; add streaming support in
LLMServiceand UI. - Consider exposing nucleus/top-k and presence/frequency penalties if backend supports them.
- Added an in-repo orchestrator that routes chat input between Chat, Packet, and Packet Filter agents. NL display-filter requests immediately update the packets pane.
- added config file support
src/pktai_tui/services/agents.py- Implemented
Orchestrator.route()with filter-first heuristic; accepts a filter only if it parses via ourLexer/Parser(prevents misrouting packet questions). - Implemented
ChatAgent,PacketAgent,PacketFilterAgentwith output sanitation (removes<think>and code fences). PacketFilterAgent.is_valid_display_filter()to validate LLM-produced filters.
- Implemented
src/pktai_tui/app.pyChatPanenow calls the orchestrator; onmode == "filter", callsapply_display_filter()and echoes:Applied display filter: <filter>.- Added
PktaiTUI.get_raw_packets()accessor used by the orchestrator flow.
src/pktai_tui/services/capture.py- Added
packets_to_text(packets, max_packets, max_chars)to build a compact textual dump of the capture for the Packet Agent context.
- Added
- NL filter: "get me all NGAP packets" → applies
ngapand the packets pane reflects filtered results; chat shows a short confirmation. - Packet analysis: "What can you tell me about this packet capture?" → routes to Packet Agent and answers based on the current capture context.
- Tune classification prompts and sampling of packet dump for very large captures.
- Add unit tests for
packets_to_text()and filter validation edge cases. - Consider showing the active display filter in the UI status area.
- Settings pane is now fully YAML-driven from
~/.pktai/pktai.yaml. The file is auto-created on first run with a fact-checked providers template.
src/pktai_tui/services/config.py- Renamed config to
pktai.yaml(under~/.pktai/). ensure_initialized()seeds providers (Perplexity, OpenAI, Together, Groq, Fireworks, OpenRouter, DeepInfra, Ollama, LM Studio, Anthropic, Google Gemini, Cohere, Mistral). Removed Azure OpenAI.
- Renamed config to
src/pktai_tui/ui/settings.py- Provider list comes only from YAML plus a “Custom” UI option.
- API keys now load from YAML on open and when provider changes.
- Save persists to YAML:
- Known providers: saves
api_keyandbase_urlunder the provider alias. - Custom providers: requires Alias + Base URL; checks alias conflicts (toast on conflict); saves
api_key,base_url,supports_list: false, andstatic_modelsparsed from a comma-separated model input. The first model is used as the session model.
- Known providers: saves
- Custom model input placeholder: “Comma-separated models (e.g., modelA, modelB)”.
- If a provider doesn’t support listing, static models from YAML populate the dropdown.
src/pktai_tui/app.py- Ensures
~/.pktai/pktai.yamlexists at startup.
- Ensures
- Press
sto open Settings. - Known provider: edit Base URL/API Key and Save to persist to YAML.
- Custom provider: enter Alias, Base URL, and comma-separated Models; Save to create a new YAML entry (with alias conflict protection).
- Reopen Settings or switch providers; saved API keys auto-populate.
- Added a hex+ASCII Data Viewer beneath the packet details
Treethat updates as you traverse fields, similar to Wireshark's data view. - Selection/highlight sync: selecting or highlighting a field in
#detailsupdates the viewer and applies a subtle highlight to the viewer body. - Parsing heuristics to derive bytes from detail lines:
- Hex sequences like
aa bb ccoraa:bb:cc. 0x...hex integers and decimal integers.- Fallback to UTF-8 bytes of the value text when needed.
- Hex sequences like
- Safety/UX guards:
- Viewer starts empty on app launch and remains empty until a capture is loaded.
- Ignores updates when no capture is loaded and when the
Treeroot node is selected.
src/pktai_tui/ui/data_viewer.py- New
DataViewerwidget rendering a classic hexdump (16 bytes/row) with left offsets and ASCII.
- New
src/pktai_tui/ui/__init__.py- Exported
DataViewer.
- Exported
src/pktai_tui/app.py- Imported and mounted
DataViewerunder#detailsin the left column. - CSS layout updated:
PacketList { height: 3fr },#details { height: 4fr },#data_viewer { height: 1fr }. - Event wiring: update viewer on
Tree.NodeHighlightedandTree.NodeSelected. - Clear viewer on mount and when packet selection changes; added
_has_capture_loaded()helper to guard updates.
- Imported and mounted
- Open a capture (press
o). Select a packet row; the detailsTreepopulates. - Move selection within
#details; the Data Viewer shows the selected field in hex + ASCII.
- Consider mapping exact byte ranges via pyshark-provided offsets for per-byte highlighting.
- Add copy-to-clipboard for hex and a byte count/length header in the viewer.