Skip to content

Commit 37bd63b

Browse files
bojielicursoragent
andcommitted
release: v0.4.0
- Bidirectional async chat REPL (subscribe + concurrent input) - Deduplicate work-log steps by (title, status) - Filter stale events by timestamp - Require pine-assistant>=0.3.0 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f44b8d8 commit 37bd63b

3 files changed

Lines changed: 30 additions & 17 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "pineai-cli"
7-
version = "0.3.0"
7+
version = "0.4.0"
88
description = "Unified CLI for Pine AI — voice calls & assistant tasks from your terminal"
99
readme = "README.md"
1010
license = "MIT"
@@ -27,7 +27,7 @@ classifiers = [
2727
]
2828
dependencies = [
2929
"pine-voice>=0.1.5",
30-
"pine-assistant>=0.2.0",
30+
"pine-assistant>=0.3.0",
3131
"click>=8.1.0",
3232
"rich>=13.0.0",
3333
]

src/pine_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Pine CLI — unified command-line interface for Pine AI."""
22

3-
__version__ = "0.3.0"
3+
__version__ = "0.4.0"

src/pine_cli/chat.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,11 @@ def _print_history_message(msg):
275275

276276

277277
class _StreamPrinter:
278-
"""Handles streaming text_part events inline; delegates everything else."""
278+
"""Handles streaming text_part events inline; deduplicates work-log steps."""
279279

280280
def __init__(self):
281281
self._in_text = False
282+
self._seen_steps: set[tuple[str, str]] = set()
282283

283284
def feed(self, event):
284285
if event.type == S2CEvent.SESSION_TEXT_PART:
@@ -290,10 +291,35 @@ def feed(self, event):
290291
self._in_text = True
291292
console.file.write(content)
292293
console.file.flush()
294+
elif event.type in (S2CEvent.SESSION_WORK_LOG, S2CEvent.SESSION_WORK_LOG_PART):
295+
self.flush()
296+
self._print_work_steps(event)
293297
else:
294298
self.flush()
295299
_print_event(event)
296300

301+
def _print_work_steps(self, event):
302+
data = event.data if isinstance(event.data, dict) else {}
303+
if event.type == S2CEvent.SESSION_WORK_LOG:
304+
steps = data.get("steps", [])
305+
else:
306+
t = data.get("step_title", "")
307+
steps = [{"step_title": t, "status": data.get("status", "")}] if t else []
308+
309+
for step in steps:
310+
title = step.get("step_title", "").strip()
311+
status = step.get("status", "").strip()
312+
if not title:
313+
continue
314+
key = (title, status)
315+
if key in self._seen_steps:
316+
continue
317+
self._seen_steps.add(key)
318+
if status:
319+
console.print(f"[dim] ● {title} \\[{status}][/dim]")
320+
else:
321+
console.print(f"[dim] ● {title}[/dim]")
322+
297323
def flush(self):
298324
if self._in_text:
299325
console.file.write("\n")
@@ -320,16 +346,3 @@ def _print_event(event):
320346
console.print(f"[dim] ● state → {state}[/dim]")
321347
elif event.type == S2CEvent.SESSION_THINKING:
322348
console.print("[dim] ● thinking…[/dim]")
323-
elif event.type == S2CEvent.SESSION_WORK_LOG:
324-
data = event.data if isinstance(event.data, dict) else {}
325-
steps = data.get("steps", [])
326-
for step in steps:
327-
title = step.get("step_title", "")
328-
status = step.get("status", "")
329-
console.print(f"[dim] ● {title} \\[{status}][/dim]" if status else f"[dim] ● {title}[/dim]")
330-
elif event.type == S2CEvent.SESSION_WORK_LOG_PART:
331-
data = event.data if isinstance(event.data, dict) else {}
332-
title = data.get("step_title", "")
333-
status = data.get("status", "")
334-
if title or status:
335-
console.print(f"[dim] ● {title} \\[{status}][/dim]" if status else f"[dim] ● {title}[/dim]")

0 commit comments

Comments
 (0)