kiwi-code 0.0.45__tar.gz → 0.0.50__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/PKG-INFO +2 -2
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/pyproject.toml +2 -2
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/client.py +5 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/commands.py +153 -4
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/sessions.py +13 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/term_dashboard.py +554 -55
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/term_dashboard_helpers.py +9 -6
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/slash_commands.py +4 -0
- kiwi_code-0.0.50/tests/test_run_steering.py +535 -0
- kiwi_code-0.0.50/tests/test_runtime_imports_on_windows.py +39 -0
- kiwi_code-0.0.50/tests/test_stats_command.py +210 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/uv.lock +5 -5
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/.gitignore +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/.python-version +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/Makefile +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/README.md +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/docs/runtime_audit.html +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/docs/social_login_e2e.md +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/scripts/set_edition.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/scripts/ws_auth_check.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/common/__init__.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/common/banner.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/_edition.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/autocode.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/checkpoints.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/edition.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/oauth.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_cli/terminal_mode.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/auth.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/checkpoints.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/cli.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/commands.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/constants.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/__init__.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/batch.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/codelens.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/context.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/handler.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/paths.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/read.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/search.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/text.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/upload.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/fs/write.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/handlers.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/link.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/restricted.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/state.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/tui_bridge.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/ui.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_runtime/uploads.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/role_mode.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/detach_files.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/term_dashboard_css.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/screens/term_dashboard_widgets.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/term_app.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/ui_settings.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/src/kiwi_tui/worktrees.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/test_hello.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/__init__.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/conftest.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_account_server.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_autocode_actions.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_batch_fs_tool.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_checkpoints.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_copy_path_fs_tool.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_fs_edit_fidelity.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_login_social.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_oauth.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_read_file_streaming.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_role_mode_injection.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_runtime_cleanup_per_account.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_runtime_connection.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_runtime_fixes.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_runtime_social_login.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_search_in_files_fs_tool.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_term_dashboard_ui.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_terminal_mode.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_tui_interactive_runtime.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_tui_palette.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_unified_diff.py +0 -0
- {kiwi_code-0.0.45 → kiwi_code-0.0.50}/tests/test_worktrees.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: kiwi-code
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.50
|
|
4
4
|
Summary: A textual-based terminal user interface application
|
|
5
5
|
Project-URL: Homepage, https://kiwicode.ai
|
|
6
6
|
Author-email: Anurag <anurag@meetkiwi.co>
|
|
@@ -19,7 +19,7 @@ Requires-Dist: httpx>=0.25.0
|
|
|
19
19
|
Requires-Dist: loguru>=0.7.3
|
|
20
20
|
Requires-Dist: psutil>=5.9.0
|
|
21
21
|
Requires-Dist: pydantic>=2.12.5
|
|
22
|
-
Requires-Dist: pykiwiai==0.0.
|
|
22
|
+
Requires-Dist: pykiwiai==0.0.14
|
|
23
23
|
Requires-Dist: setproctitle>=1.3.0
|
|
24
24
|
Requires-Dist: textual-dev>=1.8.0
|
|
25
25
|
Requires-Dist: textual>=8.1.1
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kiwi-code"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.50"
|
|
4
4
|
description = "A textual-based terminal user interface application"
|
|
5
5
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
6
6
|
requires-python = ">=3.11,<4.0"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"pykiwiai==0.0.
|
|
8
|
+
"pykiwiai==0.0.14",
|
|
9
9
|
"loguru>=0.7.3",
|
|
10
10
|
"pydantic>=2.12.5",
|
|
11
11
|
"textual>=8.1.1",
|
|
@@ -547,6 +547,11 @@ class AutobotsClientWrapper:
|
|
|
547
547
|
|
|
548
548
|
result_dict["id"] = run_id
|
|
549
549
|
|
|
550
|
+
# Prompts waiting behind the current turn: with any left, a finished turn
|
|
551
|
+
# is not the end of the run.
|
|
552
|
+
queued_inputs = getattr(getattr(result, "queued_inputs", None), "inputs", None)
|
|
553
|
+
result_dict["queued"] = len(queued_inputs) if isinstance(queued_inputs, list) else 0
|
|
554
|
+
|
|
550
555
|
logger.info(f"Parsed result: status={result_dict.get('status')}, has_result={('result' in result_dict)}")
|
|
551
556
|
logger.info(f"Result structure keys: {result_dict.keys()}")
|
|
552
557
|
if "result" in result_dict and isinstance(result_dict["result"], dict):
|
|
@@ -4,18 +4,22 @@ Each command function takes an AuthenticatedClient and keyword args,
|
|
|
4
4
|
and returns a list of strings (output lines).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import re
|
|
7
8
|
import os
|
|
8
9
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
from datetime import datetime, timedelta
|
|
11
|
-
from typing import Iterator, Optional
|
|
12
|
+
from typing import Callable, Iterator, Optional
|
|
12
13
|
from pykiwiai import AuthenticatedClient
|
|
13
14
|
from pykiwiai.api.actions import (
|
|
14
15
|
list_actions_v1_actions_get,
|
|
15
16
|
get_action_v1_actions_id_get,
|
|
16
17
|
cancel_action_run_v1_actions_action_result_id_cancel_post,
|
|
18
|
+
instruct_action_run_v1_actions_action_result_id_instruct_post,
|
|
17
19
|
)
|
|
18
20
|
from pykiwiai.api.action_results import (
|
|
21
|
+
get_action_queue_v1_action_results_action_result_id_queue_get,
|
|
22
|
+
update_action_queue_v1_action_results_action_result_id_queue_put,
|
|
19
23
|
list_action_result_v1_action_results_get,
|
|
20
24
|
get_action_result_v1_action_results_id_get,
|
|
21
25
|
)
|
|
@@ -211,6 +215,121 @@ def runs_cancel(client: AuthenticatedClient, *, id: str) -> list[str]:
|
|
|
211
215
|
return cancel_lines(id, *cancel_run(client, id=id))
|
|
212
216
|
|
|
213
217
|
|
|
218
|
+
# steering a run that is in progress
|
|
219
|
+
|
|
220
|
+
def instruct_run(client: AuthenticatedClient, *, id: str, text: str, urls: Optional[list[str]] = None) -> int:
|
|
221
|
+
"""Hand a run that is processing an instruction to read at its next step. Returns the
|
|
222
|
+
HTTP status: 202 when taken, 409 when the run is not processing."""
|
|
223
|
+
from pykiwiai.models.run_instruction import RunInstruction
|
|
224
|
+
|
|
225
|
+
resp = instruct_action_run_v1_actions_action_result_id_instruct_post.sync_detailed(
|
|
226
|
+
action_result_id=id, client=client, body=RunInstruction(text=text, urls=list(urls or [])),
|
|
227
|
+
)
|
|
228
|
+
return resp.status_code
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# the queue: prompts waiting behind the turn in progress
|
|
232
|
+
|
|
233
|
+
def get_queue(client: AuthenticatedClient, *, id: str) -> tuple[int, list[str], int]:
|
|
234
|
+
"""The prompts queued on a run, oldest first. Returns (HTTP status, prompts, the
|
|
235
|
+
queue's version, which a write has to quote)."""
|
|
236
|
+
status, blocks, version = _read_queue(client, id)
|
|
237
|
+
return status, [_block_text(block) for block in blocks], version
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _block_text(block: object) -> str:
|
|
241
|
+
return str(getattr(block, "text", None) or "")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _read_queue(client: AuthenticatedClient, id: str) -> tuple[int, list, int]:
|
|
245
|
+
"""The queue as the server stores it: (HTTP status, input blocks, version)."""
|
|
246
|
+
resp = get_action_queue_v1_action_results_action_result_id_queue_get.sync_detailed(
|
|
247
|
+
action_result_id=id, client=client,
|
|
248
|
+
)
|
|
249
|
+
queue = resp.parsed
|
|
250
|
+
inputs = getattr(queue, "inputs", None)
|
|
251
|
+
if resp.status_code != 200 or not isinstance(inputs, list):
|
|
252
|
+
return resp.status_code, [], 0
|
|
253
|
+
version = getattr(queue, "version_number", 0)
|
|
254
|
+
return 200, list(inputs), version if isinstance(version, int) else 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class QueueRemoval:
|
|
259
|
+
status: int # HTTP status of the read or the write that decided it
|
|
260
|
+
removed: Optional[str] = None # the prompt taken out, when one was
|
|
261
|
+
waiting: int = 0 # how many are queued now
|
|
262
|
+
moved: bool = False # the queue is not what the caller was looking at
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def remove_from_queue(
|
|
266
|
+
client: AuthenticatedClient, *, id: str, number: int, is_expected: Optional[Callable[[str], bool]] = None,
|
|
267
|
+
) -> QueueRemoval:
|
|
268
|
+
"""Take the prompt at `number` (1 is the next to run) out of a run's queue.
|
|
269
|
+
|
|
270
|
+
Positions shift whenever the server starts a queued prompt, so the caller can pass
|
|
271
|
+
`is_expected` to say what it believes is at that position; if something else is there
|
|
272
|
+
nothing is removed. The write quotes the version it read, so a change in between is
|
|
273
|
+
refused by the server rather than removing the wrong prompt."""
|
|
274
|
+
from pykiwiai.models.queued_inputs import QueuedInputs
|
|
275
|
+
|
|
276
|
+
status, blocks, version = _read_queue(client, id)
|
|
277
|
+
if status != 200:
|
|
278
|
+
return QueueRemoval(status)
|
|
279
|
+
if not 1 <= number <= len(blocks):
|
|
280
|
+
return QueueRemoval(200, waiting=len(blocks))
|
|
281
|
+
target = _block_text(blocks[number - 1])
|
|
282
|
+
if is_expected is not None and not is_expected(target):
|
|
283
|
+
return QueueRemoval(200, waiting=len(blocks), moved=True)
|
|
284
|
+
# The blocks go back as they came, so each keeps its author and attachments.
|
|
285
|
+
kept = blocks[: number - 1] + blocks[number:]
|
|
286
|
+
resp = update_action_queue_v1_action_results_action_result_id_queue_put.sync_detailed(
|
|
287
|
+
action_result_id=id, client=client, body=QueuedInputs(inputs=kept, version_number=version),
|
|
288
|
+
)
|
|
289
|
+
if resp.status_code == 200:
|
|
290
|
+
return QueueRemoval(200, removed=target, waiting=len(kept))
|
|
291
|
+
return QueueRemoval(resp.status_code, waiting=len(blocks), moved=resp.status_code == 409)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def clear_queue(client: AuthenticatedClient, *, id: str) -> tuple[int, int]:
|
|
295
|
+
"""Drop every prompt queued on a run. Returns (HTTP status, how many were dropped).
|
|
296
|
+
The turn in progress is not touched. A prompt the server started, or one queued from
|
|
297
|
+
elsewhere, between the read and the write makes the write a 409; it is read again
|
|
298
|
+
and retried once."""
|
|
299
|
+
from pykiwiai.models.queued_inputs import QueuedInputs
|
|
300
|
+
|
|
301
|
+
status = 409
|
|
302
|
+
for _ in range(2):
|
|
303
|
+
status, prompts, version = get_queue(client, id=id)
|
|
304
|
+
if status != 200 or not prompts:
|
|
305
|
+
return status, 0
|
|
306
|
+
resp = update_action_queue_v1_action_results_action_result_id_queue_put.sync_detailed(
|
|
307
|
+
action_result_id=id, client=client,
|
|
308
|
+
body=QueuedInputs(inputs=[], version_number=version),
|
|
309
|
+
)
|
|
310
|
+
status = resp.status_code
|
|
311
|
+
if status == 200:
|
|
312
|
+
return 200, len(prompts)
|
|
313
|
+
if status == 404:
|
|
314
|
+
# The server keeps no queue for a run nothing was ever queued on.
|
|
315
|
+
return 200, 0
|
|
316
|
+
if status != 409:
|
|
317
|
+
break
|
|
318
|
+
return status, 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def queue_lines(prompts: list[str], *, width: int = 100) -> list[str]:
|
|
322
|
+
"""The queue as numbered lines, one prompt each, cut to fit."""
|
|
323
|
+
if not prompts:
|
|
324
|
+
return ["Nothing is queued."]
|
|
325
|
+
lines = [f"{len(prompts)} queued, in the order they will run:"]
|
|
326
|
+
for number, prompt in enumerate(prompts, 1):
|
|
327
|
+
text = " ".join(prompt.split())
|
|
328
|
+
room = max(10, width - 6)
|
|
329
|
+
lines.append(f" {number}. {text if len(text) <= room else text[: room - 1] + '…'}")
|
|
330
|
+
return lines
|
|
331
|
+
|
|
332
|
+
|
|
214
333
|
# token usage
|
|
215
334
|
|
|
216
335
|
@dataclass
|
|
@@ -550,6 +669,30 @@ def subject_name(subject: dict) -> str:
|
|
|
550
669
|
return subject.get("name") or f"(unnamed {str(subject.get('subject_id') or '')[:8]})"
|
|
551
670
|
|
|
552
671
|
|
|
672
|
+
TRIGGER_LABELS = {"api": "API", "batch": "Batch", "scheduled": "Scheduled", "webhook": "Webhook"}
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def trigger_label(trigger: Optional[str]) -> str:
|
|
676
|
+
"""A run's trigger as the web dashboard names it. Runs the server could not attribute
|
|
677
|
+
to a trigger come back as `unknown`."""
|
|
678
|
+
key = (trigger or "unknown").lower()
|
|
679
|
+
return TRIGGER_LABELS.get(key) or " ".join(w.capitalize() for w in re.split(r"[_\s-]+", key) if w)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def trigger_rows(stats: RunStats) -> list[tuple[str, str, str, str, str]]:
|
|
683
|
+
"""What started the runs, largest share first: label, runs, share, tokens, spend. A run
|
|
684
|
+
spawned by another run is its own row, so what subagents cost is read off directly."""
|
|
685
|
+
rows = sorted(stats.data.get("by_trigger") or [], key=lambda x: x.get("runs") or 0, reverse=True)
|
|
686
|
+
total = sum(x.get("runs") or 0 for x in rows)
|
|
687
|
+
return [
|
|
688
|
+
(
|
|
689
|
+
trigger_label(x.get("trigger")), f"{x.get('runs') or 0:,}", fmt_pct((x.get("runs") or 0) / total),
|
|
690
|
+
fmt_count(x.get("tokens_total")), fmt_spend(x),
|
|
691
|
+
)
|
|
692
|
+
for x in rows if x.get("runs")
|
|
693
|
+
]
|
|
694
|
+
|
|
695
|
+
|
|
553
696
|
def stats_lines(stats: RunStats) -> list[str]:
|
|
554
697
|
"""The run metrics dashboard as plain output lines."""
|
|
555
698
|
if stats.status_code != 200:
|
|
@@ -597,12 +740,14 @@ def stats_lines(stats: RunStats) -> list[str]:
|
|
|
597
740
|
f"{(m.get('model') or '(unknown)')[:28]:<28} {m.get('runs', 0):>6,} "
|
|
598
741
|
f"{fmt_count(m.get('tokens_total')):>9} {fmt_spend(m):>10}"
|
|
599
742
|
)
|
|
600
|
-
triggers =
|
|
743
|
+
triggers = trigger_rows(stats)
|
|
601
744
|
if triggers:
|
|
602
|
-
|
|
745
|
+
header = f"{'Started by':<28} {'Runs':>6} {'Share':>8} {'Tokens':>9} {'Spend':>10}"
|
|
746
|
+
lines += ["", header, "-" * len(header)]
|
|
747
|
+
lines += [f"{label[:28]:<28} {runs:>6} {share:>8} {tokens:>9} {spend:>10}" for label, runs, share, tokens, spend in triggers]
|
|
603
748
|
errors = data.get("top_errors") or []
|
|
604
749
|
if errors:
|
|
605
|
-
lines
|
|
750
|
+
lines += ["", "Failures : " + ", ".join(f"{x.get('error_class') or 'Unclassified'} {x.get('runs', 0):,}" for x in errors)]
|
|
606
751
|
return lines
|
|
607
752
|
|
|
608
753
|
|
|
@@ -620,6 +765,10 @@ Session:
|
|
|
620
765
|
/new Start new conversation
|
|
621
766
|
/status Show current action & run
|
|
622
767
|
/cancel [run_id] Cancel a running run (default: current run)
|
|
768
|
+
/queue <prompt> Run a prompt after the current turn finishes
|
|
769
|
+
/queue list Show the prompts waiting to run
|
|
770
|
+
/queue remove <n> Drop one waiting prompt, by its number in /queue list
|
|
771
|
+
/queue clear Drop every prompt waiting to run
|
|
623
772
|
/stats [7d|30d|90d|1y] Show run statistics: runs, tokens, spend (default: 7d)
|
|
624
773
|
/usage [run_id] Show token usage for a run and its subagents (default: current run)
|
|
625
774
|
|
|
@@ -398,6 +398,19 @@ if not IS_WINDOWS:
|
|
|
398
398
|
except Exception:
|
|
399
399
|
return False
|
|
400
400
|
|
|
401
|
+
else:
|
|
402
|
+
|
|
403
|
+
class PTYProcess: # type: ignore[no-redef]
|
|
404
|
+
"""Stands in for the Unix PTY session on Windows, where there is no pty module.
|
|
405
|
+
|
|
406
|
+
The name has to exist so that modules importing it load on every platform; the
|
|
407
|
+
handlers only ever construct it when not on Windows, so this is never built.
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
411
|
+
raise RuntimeError("PTY sessions are not available on Windows; use PipeProcess.")
|
|
412
|
+
|
|
413
|
+
|
|
401
414
|
class PipeProcess:
|
|
402
415
|
"""Manages a persistent shell session using subprocess pipes (Windows-compatible)."""
|
|
403
416
|
|