codebind 0.6.14__py3-none-any.whl → 0.6.16__py3-none-any.whl

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.
codebind/context.md ADDED
@@ -0,0 +1 @@
1
+ Below you can find the context window information for `$model`.
codebind/context.py ADDED
@@ -0,0 +1,135 @@
1
+ """Provider-reported context usage for the active conversation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from importlib.resources import files
7
+ from string import Template
8
+ from typing import Any
9
+
10
+ import mistune
11
+ from langchain_core.language_models import BaseChatModel
12
+ from langchain_core.messages import AIMessage, BaseMessage, ToolMessage
13
+ from mistune.renderers.markdown import MarkdownRenderer
14
+
15
+
16
+ def _count(value: Any) -> int:
17
+ return value if isinstance(value, int) and value >= 0 else 0
18
+
19
+
20
+ def _text(value: str) -> dict[str, Any]:
21
+ return {"type": "text", "raw": value}
22
+
23
+
24
+ def _paragraph(value: str) -> dict[str, Any]:
25
+ return {"type": "paragraph", "children": [_text(value)]}
26
+
27
+
28
+ def _table(headers: tuple[str, ...], rows: list[tuple[str, ...]]) -> dict[str, Any]:
29
+ def cells(values: tuple[str, ...], *, head: bool) -> list[dict[str, Any]]:
30
+ return [
31
+ {
32
+ "type": "table_cell",
33
+ "attrs": {"align": "left" if index == 0 else "right", "head": head},
34
+ "children": [_text(value)],
35
+ }
36
+ for index, value in enumerate(values)
37
+ ]
38
+
39
+ return {
40
+ "type": "table",
41
+ "children": [
42
+ {"type": "table_head", "children": cells(headers, head=True)},
43
+ {
44
+ "type": "table_body",
45
+ "children": [
46
+ {"type": "table_row", "children": cells(row, head=False)} for row in rows
47
+ ],
48
+ },
49
+ ],
50
+ }
51
+
52
+
53
+ def format_context(messages: Sequence[BaseMessage], model: BaseChatModel, name: str) -> str:
54
+ """Render reported calls as Markdown; next-turn context remains an estimate."""
55
+ context_window = getattr(model, "context_window", None)
56
+ capacity = _count(context_window() if callable(context_window) else getattr(model, "context_length", 0))
57
+ calls = [message for message in messages if isinstance(message, AIMessage)]
58
+ measured = [message for message in calls if isinstance(message.usage_metadata, dict)]
59
+ results = [message for message in messages if isinstance(message, ToolMessage)]
60
+
61
+ def totals(group: list[AIMessage]) -> tuple[str, ...]:
62
+ counts = [0, 0, 0, 0]
63
+ for message in group:
64
+ usage = message.usage_metadata or {}
65
+ counts[0] += _count(usage.get("input_tokens"))
66
+ counts[1] += _count(usage.get("output_tokens"))
67
+ counts[2] += _count((usage.get("input_token_details") or {}).get("cache_read"))
68
+ counts[3] += _count((usage.get("output_token_details") or {}).get("reasoning"))
69
+ input_tokens, output_tokens, cached_tokens, reasoning_tokens = counts
70
+ cached_share = f"{cached_tokens / input_tokens:.2%}" if input_tokens else "—"
71
+ return (
72
+ f"{len(group):,}",
73
+ f"{input_tokens:,}",
74
+ f"{output_tokens:,}",
75
+ f"{cached_tokens:,} ({cached_share})",
76
+ f"{reasoning_tokens:,}",
77
+ )
78
+
79
+ last_reported = bool(calls and isinstance(calls[-1].usage_metadata, dict))
80
+ latest = calls[-1].usage_metadata if last_reported else {}
81
+ last_input = _count((latest or {}).get("input_tokens"))
82
+ last_output = _count((latest or {}).get("output_tokens"))
83
+ last_cached = _count(((latest or {}).get("input_token_details") or {}).get("cache_read"))
84
+ remaining = max(capacity - last_input - last_output, 0) if capacity and last_reported else None
85
+
86
+ def percent(value: int) -> str:
87
+ return f"{value / capacity:.2%}" if capacity and last_reported else "—"
88
+
89
+ tool_chars = sum(
90
+ len(block["text"])
91
+ for message in results
92
+ for block in message.content_blocks
93
+ if block.get("type") == "text" and isinstance(block.get("text"), str)
94
+ )
95
+ images = sum(1 for message in results for block in message.content_blocks if block.get("type") == "image")
96
+ missing = len(calls) - len(measured)
97
+ request_rows = [
98
+ ("Tool calls", *totals([message for message in measured if message.tool_calls])),
99
+ ("Responses", *totals([message for message in measured if not message.tool_calls])),
100
+ ("Reported total", *totals(measured)),
101
+ ]
102
+ if missing:
103
+ request_rows.append(("Unreported", f"{missing:,}", "—", "—", "—", "—"))
104
+ preset = Template(files("codebind").joinpath("context.md").read_text()).substitute(model=name)
105
+ parser = mistune.create_markdown(renderer="ast", plugins=["table"])
106
+ heading, *notes = parser(preset)
107
+ blocks = [
108
+ heading,
109
+ _paragraph(
110
+ "Provider-reported usage from completed calls."
111
+ if last_reported
112
+ else "Latest model call has no reported usage; remaining context cannot be measured."
113
+ if calls
114
+ else "No provider-reported model usage yet; remaining context cannot be measured."
115
+ ),
116
+ _table(
117
+ ("Last completed model call", "Tokens", "Share"),
118
+ [
119
+ ("Input (includes cached input)", f"{last_input:,}" if last_reported else "—", f"{percent(last_input)} of window" if capacity and last_reported else "—"),
120
+ ("Cached input", f"{last_cached:,}" if last_reported else "—", f"{last_cached / last_input:.2%} of input" if last_input else "—"),
121
+ ("Output (includes reasoning)", f"{last_output:,}" if last_reported else "—", f"{percent(last_output)} of window" if capacity and last_reported else "—"),
122
+ ("Estimated remaining", f"{remaining:,}" if remaining is not None else "—", f"{percent(remaining or 0)} of window" if remaining is not None else "—"),
123
+ ],
124
+ ),
125
+ _table(
126
+ ("Model calls", "Calls", "Input", "Output", "Cached input (%)", "Reasoning output"),
127
+ request_rows,
128
+ ),
129
+ _table(
130
+ ("Tool results", "Count", "Text characters", "Images"),
131
+ [("Recorded", f"{len(results):,}", f"{tool_chars:,}", f"{images:,}")],
132
+ ),
133
+ *notes,
134
+ ]
135
+ return MarkdownRenderer()(blocks, mistune.BlockState())
codebind/extension.py CHANGED
@@ -13,6 +13,8 @@ from IPython.terminal.interactiveshell import TerminalInteractiveShell
13
13
  from langchain_core.language_models import BaseChatModel
14
14
 
15
15
  from .configuration import load_configuration, load_models
16
+ from .context import format_context
17
+ from .display import display_assistant
16
18
  from .conversation import MemoryConversationStore, NotebookConversationStore
17
19
  from .jupyter import JupyterLabBridge, TARGET_NAME
18
20
  from .session import Session
@@ -26,6 +28,9 @@ def _close_state(state: dict[str, Any]) -> None:
26
28
  restore_control_interrupt = state.get("restore_control_interrupt")
27
29
  if callable(restore_control_interrupt):
28
30
  restore_control_interrupt()
31
+ restore_context_magic = state.get("restore_context_magic")
32
+ if callable(restore_context_magic):
33
+ restore_context_magic()
29
34
  restore_output_magic = state.get("restore_output_magic")
30
35
  if callable(restore_output_magic):
31
36
  restore_output_magic()
@@ -99,9 +104,31 @@ def load_ipython_extension(ipython: InteractiveShell) -> None:
99
104
  "restore_terminal_question_mode": None,
100
105
  "terminal_history": None,
101
106
  "restore_output_magic": None,
107
+ "restore_context_magic": None,
102
108
  "restore_control_interrupt": None,
103
109
  }
104
110
 
111
+ line_magics = ipython.magics_manager.magics["line"]
112
+ previous_context_magic = line_magics.get("context")
113
+
114
+ def context_magic(line: str) -> None:
115
+ if line.strip():
116
+ raise ValueError("Usage: %context")
117
+ session = state.get("session")
118
+ messages = session.messages if isinstance(session, Session) else ()
119
+ display_assistant(format_context(messages, get_model(), configuration.model))
120
+
121
+ ipython.register_magic_function(context_magic, "line", "context")
122
+
123
+ def restore_context_magic() -> None:
124
+ if line_magics.get("context") is context_magic:
125
+ if previous_context_magic is None:
126
+ line_magics.pop("context", None)
127
+ else:
128
+ line_magics["context"] = previous_context_magic
129
+
130
+ state["restore_context_magic"] = restore_context_magic
131
+
105
132
  if manager is None:
106
133
  state["session"] = Session(shell=ipython, store=MemoryConversationStore())
107
134
  if isinstance(ipython, TerminalInteractiveShell):
codebind/terminal.py CHANGED
@@ -86,7 +86,7 @@ def _history_cell(
86
86
  magic = _magic_input(transformed)
87
87
  if magic is not None and magic[0] == "question":
88
88
  return {"id": identifier, "type": "markdown", "source": magic[1]}
89
- if magic is not None and magic[0] == "output":
89
+ if magic is not None and magic[0] in {"output", "context"}:
90
90
  return {"id": identifier, "type": "raw", "source": source}
91
91
 
92
92
  outputs: list[dict[str, object]] = []
@@ -313,6 +313,11 @@ def install_question_mode(shell: TerminalInteractiveShell) -> Callable[[], None]
313
313
  if prompt_session is None:
314
314
  return lambda: None
315
315
 
316
+ original_shell_mouse = shell.mouse_support
317
+ original_prompt_mouse = prompt_session.mouse_support
318
+ # Terminal mouse reporting intercepts scrollback, even if the prompt is idle.
319
+ shell.mouse_support = False
320
+ prompt_session.mouse_support = False
316
321
  original_style = prompt_session.style
317
322
  original_prompt_for_code = shell.prompt_for_code
318
323
  had_prompt_override = "prompt_for_code" in vars(shell)
@@ -384,6 +389,8 @@ def install_question_mode(shell: TerminalInteractiveShell) -> Callable[[], None]
384
389
  else:
385
390
  del shell.prompt_for_code
386
391
  prompt_session.style = original_style
392
+ prompt_session.mouse_support = original_prompt_mouse
393
+ shell.mouse_support = original_shell_mouse
387
394
 
388
395
  return restore
389
396
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebind
3
- Version: 0.6.14
3
+ Version: 0.6.16
4
4
  Summary: A frontend-neutral model loop for persistent IPython sessions.
5
5
  Keywords: ai,ipython,llm,repl,agents
6
6
  Author: Giovanni Gravili
@@ -17,7 +17,8 @@ Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Dist: ipython>=9.17
19
19
  Requires-Dist: langchain-core>=1.0
20
- Requires-Dist: models-provider>=0.1.5
20
+ Requires-Dist: models-provider>=0.1.6
21
+ Requires-Dist: mistune>=3.3,<4
21
22
  Requires-Dist: pillow>=11
22
23
  Requires-Dist: resvg-py>=0.5
23
24
  Requires-Dist: rich>=14.1
@@ -65,7 +66,9 @@ Start a new conversation:
65
66
  uvx codebind
66
67
  ```
67
68
 
68
- Press `Shift+Tab` to switch the prompt into Question mode; the prompt turns purple. Type a question and press `Enter` to send it. Press `Shift+Tab` again to return to Python mode. Ordinary `Tab` completion is unchanged.
69
+ Press `Shift+Tab` to switch the prompt into Question mode; the prompt turns purple. Type a question and press `Enter` to send it. Press `Shift+Tab` again to return to Python mode. Ordinary `Tab` completion is unchanged. Codebind leaves terminal mouse reporting off so scrollback works while editing. Move the cursor with the keyboard or a terminal-native cursor-position shortcut, if your terminal supports one.
70
+
71
+ `%context` displays a Markdown breakdown of provider-reported model calls, including input, output, cached input (as tokens and a percentage of input), and reasoning tokens. It estimates context remaining after the last call; future inputs and tool output cannot be counted exactly until the provider responds.
69
72
 
70
73
  The `%question` and `%%question` magics remain available:
71
74
 
@@ -75,7 +78,7 @@ Inspect this project and tell me what to implement first.
75
78
  ```
76
79
 
77
80
  Terminal conversations remain live for the current IPython process. Previous terminal inputs and outputs, including Python cells you ran yourself, enter the model's context when you ask a question. Durable automatic resumption belongs to notebooks, where the notebook file provides an unambiguous conversation identity.
78
- Model-executed cells appear with IPython's native input counter, syntax highlighting, output prompts, streams, and tracebacks. Assistant Markdown appears before the following tool cell, and tool output appears as it is written. To keep terminal scrollback usable, Codebind previews at most three gray output lines, shortening each line after 60 characters with `...`. Further lines appear as `… N more output lines.` The complete output remains available to the model and in IPython history; `%output 8` opens the full text of cell 8 in IPython's pager. User-entered Python cells keep IPython's normal display behavior. Assistant Markdown is rendered through IPython's terminal MIME renderer. A terminal is a stream rather than a notebook document, so these displays are not editable notebook cells.
81
+ Model-executed cells appear with IPython's native input counter, syntax highlighting, output prompts, streams, and tracebacks. Assistant Markdown appears before the following tool cell, and tool output appears as it is written. To keep terminal scrollback usable, Codebind previews at most three gray output lines, shortening each line after 80 characters with `...`. Further lines appear as `… N more output lines.` The complete output remains available to the model and in IPython history; `%output 8` opens the full text of cell 8 in IPython's pager. User-entered Python cells keep IPython's normal display behavior. Assistant Markdown is rendered through IPython's terminal MIME renderer. A terminal is a stream rather than a notebook document, so these displays are not editable notebook cells.
79
82
 
80
83
  ## JupyterLab
81
84
 
@@ -1,24 +1,26 @@
1
1
  codebind/__init__.py,sha256=GNO_K5EPAdgiK2CWsPVVY-3bAmH2TnFJ9ReAWgSbnqM,514
2
2
  codebind/cli.py,sha256=9LOR1L3P8sPsOOgsnpbOsA1PNUZdAj1Z9ROFKeo3EV8,423
3
3
  codebind/configuration.py,sha256=b9ux-MiepzAFfRhorZp4w2pdLLs44necescIGg_IKfo,1828
4
+ codebind/context.md,sha256=Iht9nJx7drsvjXYiTm-vG-prXx1UQU4VdMgBecWubJI,64
5
+ codebind/context.py,sha256=Lo7WmsOrS1RE3Vk_Lo0Pig8IFK_-RVQOuRPVagvOziM,6051
4
6
  codebind/conversation.py,sha256=nFMe2QgFkFOA2GYInN-cO_jhA9010LpM6_Ij2g4Q2BA,17500
5
7
  codebind/display.py,sha256=IpxcElyNcr5olRIatEduN_d-sRwpsMlaq1UtrOXocQA,494
6
8
  codebind/execution.py,sha256=RBZ6RLs_iFiXBTA2VtWFuIuFN9Auf924vNeH58u1P-I,17324
7
- codebind/extension.py,sha256=XuHB3fomvdj15SwWDnawdGZkm-vJQo9rr2mI-LKsJUg,10320
9
+ codebind/extension.py,sha256=eYv-W-pSJ72fg-tyZVt3K99h_WjrGTqYEmP4jIsJ3Gg,11406
8
10
  codebind/images.py,sha256=9bJHjXc75_YRBFTAqAWiknRTCjs8ApyGVVSxC_S5lio,4342
9
11
  codebind/instructions.md,sha256=gmj5xtqVxzHPpj32XaxdvoaDwSOchqhH3Ca4aHFLqnw,4745
10
12
  codebind/jupyter.py,sha256=Chl14OBGA_EN99bEiO0rHEQJ6vLdCw-PhaC2JWwpOQ4,9551
11
13
  codebind/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
14
  codebind/session.py,sha256=TfS48Lhu9TgATo5oV5kg9H8_fSBNlOtm-kMIBSs4zW4,24143
13
- codebind/terminal.py,sha256=FGowLee5nuubsmqXZqpWxZChsw8JjwQ0wnogk9AnFRI,17615
14
- codebind-0.6.14.dist-info/licenses/LICENSE,sha256=ujX46e0LFBIBTnCDY0u3Q7LV363B7CiI7QJZxIkEXlk,1073
15
- codebind-0.6.14.data/data/share/jupyter/labextensions/codebind-jupyterlab/install.json,sha256=78rEThkfrk1pXtekX5WrG9_Y0lJYi5XBEm8n9athpFE,164
16
- codebind-0.6.14.data/data/share/jupyter/labextensions/codebind-jupyterlab/package.json,sha256=kTlYeh6jdbfq3K3EOza3bcKzhNpPO_-5Uniw-wCyfIo,1193
17
- codebind-0.6.14.data/data/share/jupyter/labextensions/codebind-jupyterlab/static/590.98c719a309e23852.js,sha256=3_28fXeGzVozV517cjkmxSSO9ccdVOe-4-RMBf46p8w,17613
18
- codebind-0.6.14.data/data/share/jupyter/labextensions/codebind-jupyterlab/static/remoteEntry.24258c9500a0850a.js,sha256=eWX4O8-R_o-6pVn1LSxUHs3LC8qfi-xX4Vg1QLwy3gg,11795
19
- codebind-0.6.14.data/data/share/jupyter/labextensions/codebind-jupyterlab/static/style.js,sha256=Opy15wyuR_ABqsrk-cOcOj2yZtvtq0r36aDkErnloD0,113
20
- codebind-0.6.14.data/data/share/jupyter/labextensions/codebind-jupyterlab/static/third-party-licenses.json,sha256=MNToQfru1YnHAie-2Virp23yWsc5co3qG0IzrO6SEfw,20
21
- codebind-0.6.14.dist-info/WHEEL,sha256=e4_1dyBeezi8ZjfxrZ3bnVOxFDa3ksqVqH0jTHkUZ3k,81
22
- codebind-0.6.14.dist-info/entry_points.txt,sha256=GiPcU63sULDlywJSKigAS4M3vaziYIrubXOb3fAg2_c,48
23
- codebind-0.6.14.dist-info/METADATA,sha256=XCYuARJmESh8_bEHrsW9RBBPMvlaxLCG-QhnpMPz3uI,8013
24
- codebind-0.6.14.dist-info/RECORD,,
15
+ codebind/terminal.py,sha256=R-nWSsZBrpxJbEmCBml7UchlV3L6FhcWctaKnaDlfFw,17999
16
+ codebind-0.6.16.dist-info/licenses/LICENSE,sha256=ujX46e0LFBIBTnCDY0u3Q7LV363B7CiI7QJZxIkEXlk,1073
17
+ codebind-0.6.16.data/data/share/jupyter/labextensions/codebind-jupyterlab/install.json,sha256=78rEThkfrk1pXtekX5WrG9_Y0lJYi5XBEm8n9athpFE,164
18
+ codebind-0.6.16.data/data/share/jupyter/labextensions/codebind-jupyterlab/package.json,sha256=kTlYeh6jdbfq3K3EOza3bcKzhNpPO_-5Uniw-wCyfIo,1193
19
+ codebind-0.6.16.data/data/share/jupyter/labextensions/codebind-jupyterlab/static/590.98c719a309e23852.js,sha256=3_28fXeGzVozV517cjkmxSSO9ccdVOe-4-RMBf46p8w,17613
20
+ codebind-0.6.16.data/data/share/jupyter/labextensions/codebind-jupyterlab/static/remoteEntry.24258c9500a0850a.js,sha256=eWX4O8-R_o-6pVn1LSxUHs3LC8qfi-xX4Vg1QLwy3gg,11795
21
+ codebind-0.6.16.data/data/share/jupyter/labextensions/codebind-jupyterlab/static/style.js,sha256=Opy15wyuR_ABqsrk-cOcOj2yZtvtq0r36aDkErnloD0,113
22
+ codebind-0.6.16.data/data/share/jupyter/labextensions/codebind-jupyterlab/static/third-party-licenses.json,sha256=MNToQfru1YnHAie-2Virp23yWsc5co3qG0IzrO6SEfw,20
23
+ codebind-0.6.16.dist-info/WHEEL,sha256=e4_1dyBeezi8ZjfxrZ3bnVOxFDa3ksqVqH0jTHkUZ3k,81
24
+ codebind-0.6.16.dist-info/entry_points.txt,sha256=GiPcU63sULDlywJSKigAS4M3vaziYIrubXOb3fAg2_c,48
25
+ codebind-0.6.16.dist-info/METADATA,sha256=fnV4XxQKFwhip_rVgmL9RWKGkt3LFzcpD5-hwQDbwyc,8548
26
+ codebind-0.6.16.dist-info/RECORD,,