localcode 0.3.2__py3-none-any.whl → 0.3.3__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.
localcode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.2"
3
+ __version__ = "0.3.3"
@@ -246,10 +246,9 @@ def _semantic_tool_summary(content: str) -> str:
246
246
  )
247
247
  if is_error:
248
248
  # Self-conditioning (arXiv:2509.09677): echoing an OLD error's text
249
- # pushes the model to repeat the mistake (scale-independent). Recent
250
- # errors stay intact (kept above by keep_recent) so it can fix the
251
- # immediate problem; older ones drop to a neutral note. The progress
252
- # ledger still records the failure fact, minus the conditioning text.
249
+ # pushes the model to repeat the mistake. Recent errors stay intact
250
+ # (kept above by keep_recent) so it can fix the immediate problem;
251
+ # older ones drop to a neutral note (the ledger still records the fact).
253
252
  return "[an earlier tool call errored and was handled — details dropped]"
254
253
  if facts:
255
254
  return (
@@ -725,9 +724,10 @@ def build_progress_ledger(
725
724
  return list(dict.fromkeys(s for s in seq if s))
726
725
 
727
726
  lines = [
728
- "## Work already done this task build on it. Do NOT re-read a file or "
729
- "re-run a command listed below unless you have CHANGED it since; you "
730
- "already have the result.",
727
+ "## A log of YOUR OWN tool calls so far this turn — NOT the user's work "
728
+ "and NOT pre-existing files on disk (leftover files you did not create are "
729
+ "not your progress or your task). Don't re-read/re-run anything below "
730
+ "unless you changed it; keep making NEW progress toward the user's goal.",
731
731
  ]
732
732
  rd = _uniq(files_read)
733
733
  if rd:
@@ -390,9 +390,11 @@ def churn_nudge_for(signal: ChurnSignal) -> str:
390
390
  )
391
391
  # INVESTIGATION_SPIN
392
392
  return (
393
- "SYSTEM: You've spent several rounds reading and searching files "
394
- "without taking any concrete action. You're investigating in circles. "
395
- "Take a concrete action NOW: make a specific edit, run the build/tests, "
396
- "or if you genuinely lack information only the user has — ask ONE "
397
- "focused question. Do not read or grep more files this round."
393
+ "SYSTEM: Several rounds of reading/listing/searching without WRITING "
394
+ "anything you're going in circles. STOP exploring. If the task is to "
395
+ "build something, create the target file and write real code THIS round "
396
+ "with write_file/edit_file; do NOT read, ls, find, cat, or grep again. "
397
+ "Leftover files on disk are NOT your progress (may be an unrelated run) — "
398
+ "only files YOU write count. If you truly lack info only the user has, "
399
+ "ask ONE focused question; otherwise start writing now."
398
400
  )
localcode/tools/bash.py CHANGED
@@ -711,6 +711,50 @@ def _normalize_repo_root_variants(cmd: str, repo: str) -> str:
711
711
  return pattern.sub(repo, cmd)
712
712
 
713
713
 
714
+ def _quoting_error_hint(output: str) -> str:
715
+ """Turn a raw shell quoting failure into actionable guidance.
716
+
717
+ A path with spaces or parentheses (e.g. `Qwen 3.6 35B-A3B (Q8)`) that
718
+ isn't quoted breaks bash with `syntax error near unexpected token` — an
719
+ error a model can't act on, so it retries the same broken shape in a loop
720
+ (observed exactly this). Tell it precisely how to fix it.
721
+ """
722
+ low = output.lower()
723
+ if "syntax error near unexpected token" not in low and "unexpected eof" not in low:
724
+ return ""
725
+ return (
726
+ "HINT: a path or argument with spaces or parentheses wasn't quoted, so "
727
+ "the shell mis-parsed it. Wrap paths in SINGLE quotes — e.g. "
728
+ "ls -la '/Users/you/My Dir (v2)'. Simpler and safer: use list_files or "
729
+ "read_file with the path as an argument — they take the raw path and "
730
+ "never need shell escaping."
731
+ )
732
+
733
+
734
+ def _redirect_shell_dir_listing(cmd: str) -> str:
735
+ """Route `ls <path>` to list_files, which can't break on spaces/parens.
736
+
737
+ Mirrors the `cat → read_file` guard. Only fires for a plain `ls` with a
738
+ PATH argument (no pipes/redirs/chains) — bare `ls`/`ls -la` in the cwd
739
+ can't hit the quoting failure, so it's left to run.
740
+ """
741
+ _cd, body = _extract_leading_cd(cmd)
742
+ b = body.strip()
743
+ if any(sep in b for sep in ("|", ">", "<", "&&", "||", ";", "$(", "`")):
744
+ return ""
745
+ # Path arg must NOT start with '-' (else `ls -la` reads its own flags as a path).
746
+ m = re.match(r"^ls((?:\s+-[a-zA-Z]+)*)\s+([^-\s].*)$", b)
747
+ if m is None:
748
+ return "" # bare `ls`/`ls -la` (no path) — harmless, let it run
749
+ path = m.group(2).strip().strip('"').strip("'")
750
+ return (
751
+ f"REJECTED: use list_files(path='{path}') to inspect a directory, not "
752
+ "bash `ls <path>`. list_files takes the path as an argument, so it never "
753
+ "breaks on spaces or parentheses in a folder name — which is what fails "
754
+ "with bash here."
755
+ )
756
+
757
+
714
758
  def execute(ctx: ToolContext, args: dict) -> str:
715
759
  cmd = _normalize_repo_root_variants(args["command"], str(ctx.repo))
716
760
  # Block ANY use of process-attaching debuggers — lldb / dtrace /
@@ -776,6 +820,9 @@ def execute(ctx: ToolContext, args: dict) -> str:
776
820
  file_read_redirect = _redirect_shell_file_read(cmd, str(repo))
777
821
  if file_read_redirect:
778
822
  return file_read_redirect
823
+ dir_listing_redirect = _redirect_shell_dir_listing(cmd)
824
+ if dir_listing_redirect:
825
+ return dir_listing_redirect
779
826
  file_write_redirect = _redirect_shell_file_write(cmd, str(repo))
780
827
  if file_write_redirect:
781
828
  return file_write_redirect
@@ -969,6 +1016,11 @@ def execute(ctx: ToolContext, args: dict) -> str:
969
1016
  pass
970
1017
  if r.returncode != 0:
971
1018
  output = f"[exit code {r.returncode}]\n{output}"
1019
+ # Shell quoting failure (unquoted spaces/parens in a path) → give
1020
+ # the model an actionable fix instead of a raw syntax error it loops on.
1021
+ _qh = _quoting_error_hint(output)
1022
+ if _qh:
1023
+ output = _qh + "\n\n" + output
972
1024
  # AirPlay-collision detector: if the model curls localhost:5000
973
1025
  # or :7000, those are squatted by macOS AirPlay Receiver /
974
1026
  # AirTunes — bare `curl -s` sees a 200 with empty body and
@@ -411,12 +411,16 @@ class _ChatTextArea(TextArea):
411
411
  & .text-area--cursor-line {
412
412
  background: ansi_default;
413
413
  }
414
- /* The caret. `reverse` swaps the cell's fg/bg using the terminal's
415
- OWN colors, so the cursor is a light block on a dark terminal and
416
- a dark block on a light one no hardcoded hex that fights the
417
- ansi_default palette (which rendered as a near-black block). */
414
+ /* The caret. textual-ansi reports dark=False, so TextArea's own
415
+ `&:light .text-area--cursor` rule (background: $foreground 70%) wins
416
+ over a plain override and renders a dark/black block. Force it with
417
+ !important: color+background = ansi_default (terminal fg/bg) plus
418
+ `reverse` swaps them, giving a light block on a dark terminal and a
419
+ dark block on a light one — terminal-adaptive, no hardcoded hex. */
418
420
  & .text-area--cursor {
419
- text-style: reverse;
421
+ color: ansi_default !important;
422
+ background: ansi_default !important;
423
+ text-style: reverse !important;
420
424
  }
421
425
  &:focus {
422
426
  background-tint: transparent 0%;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localcode
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: High-performance AI coding on consumer hardware.
5
5
  Author: LocalCode contributors
6
6
  License-Expression: Apache-2.0
@@ -1,4 +1,4 @@
1
- localcode/__init__.py,sha256=7CJ7c6EVsH8bTBDIvPtzt8awFwxSSTwMCdNZ31RVGFc,49
1
+ localcode/__init__.py,sha256=bQESHtF45jqmHUASEOi1qA8k5Ze3-o_Uh926z32yitM,49
2
2
  localcode/__main__.py,sha256=_4Tblte9F1KOpB-oGBQuTlaSN-IncLmhihqhBkpH3pU,69
3
3
  localcode/_subproc_env.py,sha256=eltq_kKvqGj22pIFivB5BZft9-s5fziBW0lsV9FhEV4,2383
4
4
  localcode/app.py,sha256=FSwpce6Jfn7zJmhL1ipaqTq0xKh0MxCMnEnMQuFV8L8,67820
@@ -67,14 +67,14 @@ localcode/voice.py,sha256=ZAh1GmJiQJF4cXQ2YCclJZB77Sa9yflox22MnDMl7t8,39178
67
67
  localcode/agent/__init__.py,sha256=-aQIBiYIojvwl9nDf_wcyiT5UQTmcEQbEBwnBEsrjEU,7349
68
68
  localcode/agent/app_tasks.py,sha256=e5tcxBZczgQyq5irGePfe5fPmNAK6AUJ_N2i11iXb5Q,5725
69
69
  localcode/agent/constants.py,sha256=0zlgyqZYJs0MOoh70rOgrXfl_ZuU5kR29Sjzrrz7TlI,12350
70
- localcode/agent/context.py,sha256=nXwOhBN5phQ8yxVPrIaju8VDngf2BmdejpvtBP8e88c,37559
70
+ localcode/agent/context.py,sha256=adV_WlFBHMDHsWdndRgrkZqRweDD9plyqMP_2X74WAw,37617
71
71
  localcode/agent/goal.py,sha256=X87Q3rsdp70wOfkpbh35M4qFWSNyklX3RuRWjv-pCXs,6264
72
72
  localcode/agent/helpers.py,sha256=vutFyCtnca0UnF0wgk7LdsqVWdiqyYm4YnFOxtnCJ_A,15004
73
73
  localcode/agent/hooks.py,sha256=XadEbSM7gHtKlLrqSKnvcQFcoWoNUXdlMdIPS-EwBGA,15841
74
74
  localcode/agent/loop.py,sha256=ZX3VS_7jCcRLX3VCSOsl8uMF51AnPICt18awXGk928s,99869
75
75
  localcode/agent/prompt_context.py,sha256=abJPLMhTzuVcsW4AfMBIW-SEqmrU69DhI-0vGZllZVU,13707
76
76
  localcode/agent/prompts.py,sha256=1VFrtzumZ2A7BaZ1gStpre-mZBs721FZi9hiB4qenZw,14639
77
- localcode/agent/recovery.py,sha256=8wwPeESsIHtdsSb_eU95JJvlVJrHxeoTVAofxmm3UIs,16639
77
+ localcode/agent/recovery.py,sha256=AA2qQwsmlgB_RV_JvQVwURj1xTxQa2c-34SljnFZeHk,16806
78
78
  localcode/agent/sections.py,sha256=9hnwaNritQHgn7qIGFBZ3ChZgMQXQcqtmpdbR1wafA0,8998
79
79
  localcode/agent/streaming.py,sha256=TEcKbmKfDGV1razrV-ceVrvIIxvS-GmLWGNDnF0gSTA,12872
80
80
  localcode/agent/tool_execution.py,sha256=yfWZwOO3f5ICd2eqw6keg8sOKOIa3vFt4peFinmu5nM,14462
@@ -99,7 +99,7 @@ localcode/tools/__init__.py,sha256=6FknaKvS-0mo4dYFTegcJcyihcJpH42NzQnM-FkKCPk,1
99
99
  localcode/tools/agent.py,sha256=aIcoGc3JiiSN4MSumwNc9TiKPZciyNXWKTYjFNir79s,8786
100
100
  localcode/tools/append_file.py,sha256=GNjPBnnV7hpD32u0Jx6YxRmsOSr4uwvJgIcInH1qhSc,2015
101
101
  localcode/tools/base.py,sha256=2QV-q1DzXWM4KL9mFKNPzlBiLOS2uKLXhlgGyeWEKCI,2064
102
- localcode/tools/bash.py,sha256=20xxJAmqSmtkgTbvm_v9qyyK-7mcy_LB28EVLjsuyvg,40970
102
+ localcode/tools/bash.py,sha256=Yuuxo0lsX0JuFbHQX8OflmgZgq7uqBc1FUSGJRHlsB8,43380
103
103
  localcode/tools/edit_diff.py,sha256=eJ1FyK3HecAmJNiM-XdHOQ7WCkyOuD6hwAlTVgYQGGk,3017
104
104
  localcode/tools/edit_file.py,sha256=DhhQSi6wnD4_IwLEYoC-zBLUPaxhihZxBatNoerFLe4,13455
105
105
  localcode/tools/facts.py,sha256=OqTLYOclKy6lpPQiU3aDrzY-Ho-jhWc8mvzLx6TFRCk,5576
@@ -119,7 +119,7 @@ localcode/tui/__init__.py,sha256=Yfy9bvw3l1iY4FDhRX8qZVgnZLuYiq8lc9SDYl4cWwc,61
119
119
  localcode/tui/app.py,sha256=KjQzpGKnxkgh-0kVc3zcb63Q6a5LBuGreBqTUrasiqM,17700
120
120
  localcode/tui/bridge.py,sha256=urPPCqC_nisAYawWk3juvMI8IaoJfv4g7USpGOZOqQc,2803
121
121
  localcode/tui/screens/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
122
- localcode/tui/screens/chat.py,sha256=aTYu03GTAlbXFuZDZaZBIvGNE4we--nRHZw5OYRgvAw,193390
122
+ localcode/tui/screens/chat.py,sha256=3eRHyiFNec7cbWEcClEEopzCDt3_SGP9lIkNH5yuSCc,193659
123
123
  localcode/tui/screens/mode_picker.py,sha256=cK_xNo7ytOwJXJwzUevMTcb5dwQKtjMfh60of-ntRz4,2347
124
124
  localcode/tui/screens/model_picker.py,sha256=VkhyREIT2W1J8A6tUe0ix5TbWXUa2XSe9qKzcXgjjz4,35991
125
125
  localcode/tui/screens/setup.py,sha256=6sSKU1KagLyf5DtkQWOSBA7inPFZMTF570t-20PIwaE,40844
@@ -131,9 +131,9 @@ localcode/tui/widgets/chat_log.py,sha256=D5DUkrQXL_ilxyC8N1xIPLw4Pqeh0jSWt1mPgho
131
131
  localcode/tui/widgets/voice_visualizer.py,sha256=zOhrIxaMcg-F2Y8LvHJTddq227phmiTjtEyRbkx1XxU,4709
132
132
  localcode/tui/widgets/messages/__init__.py,sha256=952AZ1qVMGUIqPIry7mwxnYbMkgPlcY5FAwZtEa5YPg,967
133
133
  localcode/tui/widgets/messages/diff.py,sha256=khDf_MThrQz0S62-t-m7i3OU5cdNolUk1TCeKMRPDQI,10910
134
- localcode-0.3.2.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
135
- localcode-0.3.2.dist-info/METADATA,sha256=NfiKTcmNdZeRHzlMdJd6Qm5lTlLT9W2ZPWEnMYsZBKU,7813
136
- localcode-0.3.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
137
- localcode-0.3.2.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
138
- localcode-0.3.2.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
139
- localcode-0.3.2.dist-info/RECORD,,
134
+ localcode-0.3.3.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
135
+ localcode-0.3.3.dist-info/METADATA,sha256=22UXuP5QSAS0stB-2Nb7uUKNUlwKJPnnN7CnWmo6v2Y,7813
136
+ localcode-0.3.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
137
+ localcode-0.3.3.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
138
+ localcode-0.3.3.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
139
+ localcode-0.3.3.dist-info/RECORD,,